diff --git a/docs/engineering/architecture.md b/docs/engineering/architecture.md
index 4c8e66c..85229c1 100644
--- a/docs/engineering/architecture.md
+++ b/docs/engineering/architecture.md
@@ -27,7 +27,11 @@ boundaries, optional generated error documents, concrete and dynamic
request-time SSR pages with declared `load {}` fields, safe local load
redirects, and inline `go ssr {}` load handlers through the generated
request-time route lane.
-The app generator uses typed IR and Go AST/printer output before `go/format`.
+Backend adapter generation lowers request-time endpoint metadata into typed
+appgen IR before emitting imports, route registrations, request decoders,
+handler calls, response writing, fallback metadata, split frontend proxy route
+matching, and backend-only app routing. The app generator uses typed IR and Go
+AST/printer output before `go/format`.
`runtime/contracts` now provides the first local typed registry for queries,
commands, backend-owned domain and integration events, presentation events, and
@@ -159,7 +163,7 @@ manifest report (`internal/lang/testdata/manifest_golden`).
| `internal/project` | Load project-level config, module source groups, build targets, and future source roots. | Compiler | SPA `gowdk.config.go` subset implemented for build discovery, output, and `Build.Targets`; project-level CLI commands require this config or an explicit `--config` file before compiling `.gwdk` code. |
| `internal/compiler` | Validate manifests and coordinate compilation metadata. | Compiler | Render-mode, duplicate identity, redundant component implementation, component Go contract, saved default `go {}` package type-checking with sibling Go files, route shape, duplicate route param, duplicate route pattern, route-method, required page-view validation, default `go {}` backend endpoint binding fallback, and `go/packages`-backed backend binding implemented. CLI route/endpoint reports now convert through `internal/gwdkir.Program`. |
| `internal/buildgen` | Emit route-derived spa HTML files for build-time pages and SSR render artifacts. | Compiler | Disk builds, memory builds, incremental SPA builds, and SSR artifact planning consume `internal/gwdkir.Program`. Initial simple page, literal build data, imported Go build data calls, literal dynamic path expansion, component expansion, partial runtime asset emission, default JS island asset emission, component-level non-CSS asset emission, component-level WASM island asset emission, page-level `go client {}` WASM mount asset emission, concrete and dynamic SSR page rendering with declared `load {}` placeholders, route manifest emission, asset manifest emission, OpenAPI report emission, mandatory build report emission with cache-policy and request-time skip events, identical-output write skipping, and incremental changed-page spa rendering implemented. |
-| `internal/appgen` | Emit generated Go app source for embedded spa output and request-time routes. | Compiler | Auto route planning consumes `internal/gwdkir.Program`, backend adapter planning uses typed appgen IR, and generated app Go files are assembled with `go/ast`/`go/printer` before `go/format`. Generates `go.mod`, `main.go`, copied spa assets, thin `runtime/app` server wiring, `runtime/app.BackendRouter` registrations for feature-bound action/API/fragment routes, 501 stubs for missing/unsupported handlers, POST redirect and partial fragment action handlers backed by `runtime/form`, `runtime/response`, `runtime/validation`, and `addons/partial`, form input decoders, concrete and dynamic standalone fragment routes, concrete and dynamic SSR route handlers backed by `runtime/route`, declared SSR load path calls with redirect/error-page handling through `addons/ssr`, shared request-time guard checks through `runtime/guard`, generated `gowdk_go/` packages for default `go {}` and `go ssr {}` blocks, addon `GoBlockConsumer` Go files, split backend apps, command/query contract exposure metadata in adapter IR including runtime roles, identical-output write skipping, stale embedded spa cleanup, and can invoke `go build` for local binaries or Go `js/wasm` artifacts. |
+| `internal/appgen` | Emit generated Go app source for embedded spa output and request-time routes. | Compiler | Auto route planning consumes `internal/gwdkir.Program`, backend adapter planning uses typed appgen IR, and generated app Go files are assembled with `go/ast`/`go/printer` before `go/format`. Generates `go.mod`, `main.go`, copied spa assets, thin `runtime/app` server wiring, `runtime/app.BackendRouter` registrations for feature-bound action/API/fragment/contract routes, split frontend proxy route matching from the same backend metadata, backend-only app routing, 501 stubs for missing/unsupported handlers, POST redirect and partial fragment action handlers backed by `runtime/form`, `runtime/response`, `runtime/validation`, and `addons/partial`, form input decoders, concrete and dynamic standalone fragment routes, concrete and dynamic SSR route handlers backed by `runtime/route`, declared SSR load path calls with redirect/error-page handling through `addons/ssr`, shared request-time guard checks through `runtime/guard`, generated `gowdk_go/` packages for default `go {}` and `go ssr {}` blocks, addon `GoBlockConsumer` Go files, split backend apps, command/query contract exposure metadata in adapter IR including runtime roles, identical-output write skipping, stale embedded spa cleanup, and can invoke `go build` for local binaries or Go `js/wasm` artifacts. |
| `internal/clientrt` | Emit client runtime for partial updates and static-first SPA navigation. | Runtime | First partial form enhancement runtime emits lifecycle hooks, target/swap request headers, swaps, focus restoration, loading state metadata, island remounts, and page-level `go client {}` remounts after SPA navigation. |
| `runtime/render` | Core rendering engine used by static output, actions, partials, and SSR. | Runtime | Renderer and generated-code builder implemented; expression text writes escape by default. |
| `runtime/component` | Generated component runtime contract. | Runtime | Initial component interface implemented. |
diff --git a/docs/engineering/milestone-8-generated-adapter-ir-plan.md b/docs/engineering/milestone-8-generated-adapter-ir-plan.md
new file mode 100644
index 0000000..d987d32
--- /dev/null
+++ b/docs/engineering/milestone-8-generated-adapter-ir-plan.md
@@ -0,0 +1,81 @@
+# Implementation Plan: Milestone 8 Generated Adapter IR
+
+## Context
+
+Spec: `docs/engineering/milestone-8-generated-adapter-ir-spec.md`
+
+Roadmap step 8: Generated adapter IR.
+
+Relevant ADRs: 0002 compile-first render model, 0005 generated Go emission
+boundary, 0006 GOWDK Compiler and Runtime boundary.
+
+## Assumptions
+
+- Existing endpoint slices remain the accepted public `appgen.Options` input for
+ now, but generator internals should lower them into `BackendAdapterIR` once
+ and use the IR for backend route decisions.
+- SSR route generation can stay in its current path because roadmap step 12 owns
+ request-time page rendering.
+
+## Proposed Changes
+
+- Expand `BackendAdapterIR` with endpoint guard/import metadata and helpers for
+ routable registrations, dynamic registrations, backend imports, and guard
+ names.
+- Update generated backend router and split-proxy route matching to consume
+ adapter IR registrations instead of raw action/API/fragment slices.
+- Update backend-sensitive import, CSRF, guard, rate-limit, and backend route
+ presence checks to consume adapter IR where practical.
+- Add focused tests for adapter IR metadata and split-proxy route matching.
+- Update generated app golden output only if generated source ordering changes.
+- Mark roadmap step 8 as implemented when the acceptance criteria are covered.
+
+## Files Expected To Change
+
+- `internal/appgen/adapter_ir.go`
+- `internal/appgen/source.go`
+- `internal/appgen/source_backend.go`
+- `internal/appgen/source_guards.go`
+- `internal/appgen/source_rate_limit.go`
+- `internal/appgen/adapter_ir_test.go`
+- `internal/appgen/appgen_test.go`
+- `docs/product/roadmap.md`
+- `docs/engineering/architecture.md`
+
+## Data And API Impact
+
+- No public API change.
+- No manifest JSON shape change.
+- Generated Go behavior should stay compatible.
+
+## Tests
+
+- Unit: `go test ./internal/appgen`
+- Integration: `go test ./internal/compiler ./internal/buildgen ./internal/appgen`
+- End-to-end: `go run ./cmd/gowdk build --out /tmp/gowdk-m8-build --app /tmp/gowdk-m8-app examples/pages/*.gwdk`
+- Manual: inspect generated app source for backend router registration through
+ `runtime/app.BackendRouter`.
+
+## Verification Commands
+
+```sh
+go test ./internal/appgen
+go test ./internal/compiler ./internal/buildgen ./internal/appgen
+go build ./cmd/gowdk
+go run ./cmd/gowdk build --out /tmp/gowdk-m8-build --app /tmp/gowdk-m8-app examples/pages/*.gwdk
+scripts/test-go-modules.sh
+```
+
+## Rollback Plan
+
+- Revert the adapter IR migration commits. Existing endpoint-slice generation
+ paths are preserved by tests and can be restored without data migration.
+
+## Risks
+
+- Import pruning can accidentally drop runtime packages needed only by generated
+ proxy or compatibility paths.
+- Route matching order can shift for split proxy output if registrations are not
+ sorted consistently.
+- Broad helper changes can affect generated app goldens outside the intended
+ backend route path.
diff --git a/docs/engineering/milestone-8-generated-adapter-ir-spec.md b/docs/engineering/milestone-8-generated-adapter-ir-spec.md
new file mode 100644
index 0000000..4e29a96
--- /dev/null
+++ b/docs/engineering/milestone-8-generated-adapter-ir-spec.md
@@ -0,0 +1,94 @@
+# Feature Spec: Milestone 8 Generated Adapter IR
+
+## Problem
+
+Generated app output supports one-binary, split frontend proxy, and backend-only
+artifacts, but some backend decisions still read raw action/API/fragment option
+slices instead of the typed backend adapter IR. That makes route registration,
+proxy route matching, guards, rate limits, CSRF, imports, and fallback behavior
+harder to reason about as endpoint kinds grow.
+
+## Goals
+
+- Make backend adapter generation use one typed IR for endpoint registrations,
+ request decoding metadata, handler calls, response metadata, and fallback
+ metadata.
+- Keep generated app, split frontend proxy, and backend-only generation on the
+ same backend route metadata.
+- Preserve current generated app behavior and public runtime contracts.
+- Keep generated Go emitted through AST/printer/format.
+
+## Non-Goals
+
+- Do not add new public `.gwdk` syntax.
+- Do not change route manifest or asset manifest JSON shapes.
+- Do not remove compatibility fallback hooks in `runtime/app`.
+- Do not migrate SSR route generation into this backend adapter IR slice.
+
+## Users And Permissions
+
+- Primary users: GOWDK maintainers and app authors who inspect generated Go.
+- Roles or permissions: generated route guards and rate limiting keep their
+ existing semantics.
+- Data visibility rules: generated error responses continue to hide ordinary
+ 5xx handler details and avoid exposing secrets.
+
+## User Flow
+
+1. An app author declares actions, APIs, fragments, or web contract references.
+2. The compiler validates bindings and builds endpoint metadata.
+3. App generation lowers backend metadata into `BackendAdapterIR`.
+4. One-binary, split proxy, or backend-only output uses that IR to register,
+ match, guard, limit, and dispatch request-time backend routes.
+
+## Requirements
+
+### Functional
+
+- Backend route registration must be derived from `BackendAdapterIR`.
+- Split frontend proxy route matching must use the same registration metadata
+ as backend route registration.
+- Guard, rate-limit, CSRF, and backend import decisions must be derived from
+ adapter IR where they depend on backend endpoint metadata.
+- Bound action/API/fragment handler calls and missing/unsupported fallbacks must
+ remain represented in adapter IR and covered by tests.
+
+### Non-Functional
+
+- Performance: route matching remains a simple generated switch plus existing
+ dynamic fragment matcher.
+- Reliability: generated app source remains gofmt-formatted and golden-tested.
+- Accessibility: no user-visible markup changes.
+- Security/privacy: body limits, CSRF ordering, guard checks, rate limits, and
+ no-store error behavior stay unchanged.
+- Observability: generated route metadata remains inspectable in the generated
+ source and existing CLI reports.
+
+## Acceptance Criteria
+
+- [x] `internal/appgen` can prove adapter IR contains registrations, decoders,
+ handler calls, responses, fallbacks, guards, imports, and dynamic-route flags.
+- [x] Generated app and backend-only source use adapter IR for backend router
+ construction.
+- [x] Split frontend proxy source uses adapter IR for backend route matching.
+- [x] Existing generated app goldens and appgen tests pass.
+- [x] The roadmap step 8 row is accurate after implementation.
+
+## Edge Cases
+
+- Proxy frontend builds must not import user backend handler packages.
+- Dynamic fragment routes still need runtime route matching in split proxy
+ output.
+- Public guards are omitted from runtime guard execution.
+- Guardless request-time pages remain handled by the existing SSR/page route
+ logic, not the backend adapter IR.
+
+## Dependencies
+
+- Internal: `internal/appgen`, `internal/gwdkir`, `runtime/app`.
+- External: none.
+
+## Open Questions
+
+- None for this slice. SSR route unification remains a later generated-output
+ cleanup, not part of milestone 8.
diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md
index 53e70b8..c51a985 100644
--- a/docs/product/roadmap.md
+++ b/docs/product/roadmap.md
@@ -129,6 +129,10 @@ level, the current baseline already includes:
- shared backend routing primitives in `runtime/app`, runtime action/API
adapter helpers, one generated backend hook, request body limits, and no-store
defaults for request-time responses;
+- typed backend adapter IR driving generated action/API/fragment/contract route
+ registrations, backend imports, split frontend proxy route matching,
+ backend-only route presence, guard/rate-limit/CSRF endpoint checks, and
+ generated `501` fallback metadata;
- first-slice action/API execution, partial fragment responses, dynamic
standalone fragment routes, and concrete or dynamic request-time SSR pages
with declared `load {}` fields through buildgen, appgen, `runtime/app`, and
@@ -159,7 +163,7 @@ are stable.
| 5 | Unified endpoint metadata | Actions and APIs normalize into one framework-neutral endpoint model containing source, kind, package path, package name, symbol, method, path, signature kind, input type, source span, and binding status. Route metadata remains limited to static, SPA, SSR, and hybrid page routes. |
| 6 | Endpoint discovery policy | Optional Go endpoint comments such as `//gowdk:act POST /login` and `//gowdk:api GET /api/session` can feed the same endpoint model. The compiler never auto-discovers endpoints by function name and never scans Gin/Echo/Fiber route registration as a source of truth. Conflicts are hard diagnostics. |
| 7 | Binding severity policy | Missing or unsupported handlers can remain non-fatal in dev/migration mode, but strict production builds fail unless an explicit stub flag allows `501` output. Feature packages are documented as not importing generated app output. |
-| 8 | Generated adapter IR | Backend adapter generation is driven by typed IR for imports, endpoint registrations, request decoding, handler calls, response writing, and `501` fallbacks. One-binary, split frontend proxy, and backend-only app generation consume the same metadata. |
+| 8 | Generated adapter IR | Implemented. Backend adapter generation is driven by typed IR for imports, endpoint registrations, request decoding, handler calls, response writing, and `501` fallbacks. One-binary, split frontend proxy, and backend-only app generation consume the same backend metadata. |
| 9 | Go AST generation cleanup | API handlers, backend route registration, app shells, embed wiring, split app code, and remaining generated Go move to `go/ast`/`go/printer` plus `go/format`. Hardcoded line writing and source snippets are banned except for documented temporary exceptions. |
| 10 | Secure actions and forms | Generated action adapters wire CSRF token generation and validation, define token exposure, invalid-CSRF status/body shape, submit-button intent handling, validation fragment patterns, and production-safe action/API docs. |
| 11 | Guards and runtime context | Generated guards work for SSR, actions, and APIs. The request context helper contract is documented around `context.Context`, `app.Request(ctx)`, `app.Params(ctx)`, `app.CSRF(ctx)`, and `app.Session(ctx)`, or the project deliberately switches to an explicit app context. |
diff --git a/internal/appgen/adapter_ir.go b/internal/appgen/adapter_ir.go
index b703e9c..6c8cb5a 100644
--- a/internal/appgen/adapter_ir.go
+++ b/internal/appgen/adapter_ir.go
@@ -9,6 +9,9 @@ import (
type BackendAdapterIR struct {
Registrations []BackendEndpointRegistration
+ Actions []BackendActionAdapter
+ APIs []BackendAPIAdapter
+ Fragments []BackendFragmentAdapter
ContractExposures []BackendContractExposure
Decoders []BackendDecoder
Calls []BackendHandlerCall
@@ -33,6 +36,57 @@ type BackendEndpointRegistration struct {
Handler string
PageID string
Name string
+ Guards []string
+ Dynamic bool
+}
+
+type BackendActionAdapter struct {
+ Endpoint BackendEndpointRegistration
+ PageID string
+ ActionName string
+ Method string
+ Route string
+ Guards []string
+ InputName string
+ InputType string
+ InputFields []string
+ RequiredFields []string
+ RequiredMessages map[string]string
+ ValidationRules []ActionValidationRule
+ ValidatesInput bool
+ Redirect string
+ Fragments []ActionFragment
+ ErrorPage string
+ Binding source.BackendBinding
+ BackendAlias string
+}
+
+type BackendAPIAdapter struct {
+ Endpoint BackendEndpointRegistration
+ PageID string
+ APIName string
+ Method string
+ Route string
+ Guards []string
+ ErrorPage string
+ Binding source.BackendBinding
+ BackendAlias string
+}
+
+type BackendFragmentAdapter struct {
+ Endpoint BackendEndpointRegistration
+ PageID string
+ FragmentName string
+ Method string
+ Route string
+ RouteParams []source.RouteParam
+ Target string
+ HTML string
+ Package string
+ Uses map[string]string
+ Guards []string
+ Binding source.BackendBinding
+ BackendAlias string
}
type BackendDecoder struct {
@@ -43,11 +97,12 @@ type BackendDecoder struct {
}
type BackendHandlerCall struct {
- Endpoint BackendEndpointRegistration
- Alias string
- Function string
- Signature source.BackendSignatureKind
- InputType string
+ Endpoint BackendEndpointRegistration
+ Alias string
+ ImportPath string
+ Function string
+ Signature source.BackendSignatureKind
+ InputType string
}
type BackendResponse struct {
@@ -93,8 +148,31 @@ func backendAdapterIR(options Options) BackendAdapterIR {
Handler: "action",
PageID: action.PageID,
Name: action.ActionName,
+ Guards: append([]string(nil), action.Guards...),
+ Dynamic: backendRouteIsDynamic(action.Route),
+ }
+ actionAdapter := BackendActionAdapter{
+ Endpoint: endpoint,
+ PageID: action.PageID,
+ ActionName: action.ActionName,
+ Method: action.Method,
+ Route: action.Route,
+ Guards: append([]string(nil), action.Guards...),
+ InputName: action.InputName,
+ InputType: action.InputType,
+ InputFields: append([]string(nil), action.InputFields...),
+ RequiredFields: append([]string(nil), action.RequiredFields...),
+ RequiredMessages: copyStringMap(action.RequiredMessages),
+ ValidationRules: append([]ActionValidationRule(nil), action.ValidationRules...),
+ ValidatesInput: action.ValidatesInput,
+ Redirect: action.Redirect,
+ Fragments: append([]ActionFragment(nil), action.Fragments...),
+ ErrorPage: action.ErrorPage,
+ Binding: action.Binding,
+ BackendAlias: action.BackendAlias,
}
ir.Registrations = append(ir.Registrations, endpoint)
+ ir.Actions = append(ir.Actions, actionAdapter)
if action.InputType != "" || action.Binding.InputType != "" {
decoder := BackendDecoder{
Endpoint: endpoint,
@@ -102,20 +180,21 @@ func backendAdapterIR(options Options) BackendAdapterIR {
Fields: append([]string(nil), action.InputFields...),
}
if action.Binding.Status == source.BackendBindingBound && action.Binding.InputType != "" {
- decoder.Function = boundActionDecoderName(action)
+ decoder.Function = boundActionDecoderName(actionAdapter)
decoder.Input = action.Binding.InputType
} else if action.InputType != "" {
- decoder.Function = actionDecoderName(action)
+ decoder.Function = actionDecoderName(actionAdapter)
}
ir.Decoders = append(ir.Decoders, decoder)
}
if action.Binding.Status == source.BackendBindingBound {
ir.Calls = append(ir.Calls, BackendHandlerCall{
- Endpoint: endpoint,
- Alias: action.BackendAlias,
- Function: action.Binding.FunctionName,
- Signature: action.Binding.Signature,
- InputType: action.Binding.InputType,
+ Endpoint: endpoint,
+ Alias: action.BackendAlias,
+ ImportPath: action.Binding.ImportPath,
+ Function: action.Binding.FunctionName,
+ Signature: action.Binding.Signature,
+ InputType: action.Binding.InputType,
})
}
if action.Binding.Status != "" && action.Binding.Status != source.BackendBindingBound {
@@ -140,14 +219,29 @@ func backendAdapterIR(options Options) BackendAdapterIR {
Handler: "api",
PageID: api.PageID,
Name: api.APIName,
+ Guards: append([]string(nil), api.Guards...),
+ Dynamic: backendRouteIsDynamic(api.Route),
+ }
+ apiAdapter := BackendAPIAdapter{
+ Endpoint: endpoint,
+ PageID: api.PageID,
+ APIName: api.APIName,
+ Method: api.Method,
+ Route: api.Route,
+ Guards: append([]string(nil), api.Guards...),
+ ErrorPage: api.ErrorPage,
+ Binding: api.Binding,
+ BackendAlias: api.BackendAlias,
}
ir.Registrations = append(ir.Registrations, endpoint)
+ ir.APIs = append(ir.APIs, apiAdapter)
if api.Binding.Status == source.BackendBindingBound {
ir.Calls = append(ir.Calls, BackendHandlerCall{
- Endpoint: endpoint,
- Alias: api.BackendAlias,
- Function: api.Binding.FunctionName,
- Signature: api.Binding.Signature,
+ Endpoint: endpoint,
+ Alias: api.BackendAlias,
+ ImportPath: api.Binding.ImportPath,
+ Function: api.Binding.FunctionName,
+ Signature: api.Binding.Signature,
})
}
if api.Binding.Status != "" && api.Binding.Status != source.BackendBindingBound {
@@ -167,8 +261,42 @@ func backendAdapterIR(options Options) BackendAdapterIR {
Handler: "fragment",
PageID: fragment.PageID,
Name: fragment.FragmentName,
+ Guards: append([]string(nil), fragment.Guards...),
+ Dynamic: backendRouteIsDynamic(fragment.Route),
+ }
+ fragmentAdapter := BackendFragmentAdapter{
+ Endpoint: endpoint,
+ PageID: fragment.PageID,
+ FragmentName: fragment.FragmentName,
+ Method: fragment.Method,
+ Route: fragment.Route,
+ RouteParams: append([]source.RouteParam(nil), fragment.RouteParams...),
+ Target: fragment.Target,
+ HTML: fragment.HTML,
+ Package: fragment.Package,
+ Uses: copyStringMap(fragment.Uses),
+ Guards: append([]string(nil), fragment.Guards...),
+ Binding: fragment.Binding,
+ BackendAlias: fragment.BackendAlias,
}
ir.Registrations = append(ir.Registrations, endpoint)
+ ir.Fragments = append(ir.Fragments, fragmentAdapter)
+ if fragment.Binding.Status == source.BackendBindingBound {
+ ir.Calls = append(ir.Calls, BackendHandlerCall{
+ Endpoint: endpoint,
+ Alias: fragment.BackendAlias,
+ ImportPath: fragment.Binding.ImportPath,
+ Function: fragment.Binding.FunctionName,
+ Signature: fragment.Binding.Signature,
+ })
+ }
+ if fragment.Binding.Status != "" && fragment.Binding.Status != source.BackendBindingBound {
+ ir.Fallbacks = append(ir.Fallbacks, BackendFallback{
+ Endpoint: endpoint,
+ Status: fragment.Binding.Status,
+ Message: fragment.Binding.Message,
+ })
+ }
ir.Responses = append(ir.Responses, BackendResponse{
Endpoint: endpoint,
NoStore: true,
@@ -184,6 +312,8 @@ func backendAdapterIR(options Options) BackendAdapterIR {
Handler: string(ref.Kind),
PageID: ref.OwnerID,
Name: ref.Name,
+ Guards: append([]string(nil), ref.Guards...),
+ Dynamic: backendRouteIsDynamic(ref.Path),
}
ir.ContractExposures = append(ir.ContractExposures, BackendContractExposure{
Endpoint: endpoint,
@@ -213,6 +343,55 @@ func (ir BackendAdapterIR) HasRegistrations() bool {
return len(ir.Registrations) > 0 || len(routableContractExposures(ir.ContractExposures)) > 0
}
+func (ir BackendAdapterIR) HasEndpointKind(kind BackendEndpointKind) bool {
+ for _, registration := range ir.Registrations {
+ if registration.Kind == kind {
+ return true
+ }
+ }
+ for _, exposure := range routableContractExposures(ir.ContractExposures) {
+ if exposure.Endpoint.Kind == kind {
+ return true
+ }
+ }
+ return false
+}
+
+func (ir BackendAdapterIR) HasDynamicRoutes() bool {
+ for _, registration := range ir.Registrations {
+ if registration.Dynamic {
+ return true
+ }
+ }
+ for _, exposure := range routableContractExposures(ir.ContractExposures) {
+ if exposure.Endpoint.Dynamic {
+ return true
+ }
+ }
+ return false
+}
+
+func (ir BackendAdapterIR) GuardNames() []string {
+ var guards []string
+ for _, registration := range ir.Registrations {
+ guards = append(guards, registration.Guards...)
+ }
+ for _, exposure := range routableContractExposures(ir.ContractExposures) {
+ guards = append(guards, exposure.Guards...)
+ }
+ return guards
+}
+
+func (ir BackendAdapterIR) BackendImports() map[string]string {
+ imports := map[string]string{}
+ for _, call := range ir.Calls {
+ if call.ImportPath != "" && call.Alias != "" {
+ imports[call.ImportPath] = call.Alias
+ }
+ }
+ return imports
+}
+
func backendContractEndpointKind(kind gwdkir.ContractKind) BackendEndpointKind {
switch kind {
case gwdkir.ContractQuery:
@@ -238,3 +417,18 @@ func sortedContractReferences(refs []gwdkir.ContractReference) []gwdkir.Contract
})
return out
}
+
+func backendRouteIsDynamic(route string) bool {
+ return len(ssrRoutePatternParams(route)) > 0
+}
+
+func copyStringMap(in map[string]string) map[string]string {
+ if len(in) == 0 {
+ return nil
+ }
+ out := make(map[string]string, len(in))
+ for key, value := range in {
+ out[key] = value
+ }
+ return out
+}
diff --git a/internal/appgen/adapter_ir_test.go b/internal/appgen/adapter_ir_test.go
index 470d13f..8aa00ac 100644
--- a/internal/appgen/adapter_ir_test.go
+++ b/internal/appgen/adapter_ir_test.go
@@ -14,11 +14,16 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
ActionName: "Subscribe",
Method: "POST",
Route: "/newsletter",
+ Guards: []string{"auth.required"},
InputType: "SubscribeInput",
InputFields: []string{"email"},
- Redirect: "/newsletter?ok=1",
+ RequiredMessages: map[string]string{
+ "email": "Email is required",
+ },
+ Redirect: "/newsletter?ok=1",
Binding: source.BackendBinding{
Status: source.BackendBindingBound,
+ ImportPath: "example.com/app/newsletter",
FunctionName: "Subscribe",
Signature: source.BackendSignatureActionForm,
InputType: "SubscribeInput",
@@ -32,6 +37,7 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
Route: "/api/health",
Binding: source.BackendBinding{
Status: source.BackendBindingBound,
+ ImportPath: "example.com/app/status",
FunctionName: "Health",
Signature: source.BackendSignatureAPI,
},
@@ -41,33 +47,63 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
PageID: "patients",
FragmentName: "List",
Method: "GET",
- Route: "/patients/list",
+ Route: "/patients/{id}/list",
Target: "#patients",
HTML: "",
+ Binding: source.BackendBinding{
+ Status: source.BackendBindingBound,
+ ImportPath: "example.com/app/patients",
+ FunctionName: "List",
+ Signature: source.BackendSignatureFragment,
+ },
+ BackendAlias: "patients",
}},
})
if len(ir.Registrations) != 3 {
t.Fatalf("expected action, API, and fragment registrations, got %#v", ir.Registrations)
}
+ if len(ir.Actions) != 1 || ir.Actions[0].Endpoint.Path != ir.Registrations[0].Path || ir.Actions[0].Endpoint.Kind != BackendEndpointAction || ir.Actions[0].RequiredMessages["email"] != "Email is required" {
+ t.Fatalf("expected action adapter metadata, got %#v", ir.Actions)
+ }
+ if len(ir.APIs) != 1 || ir.APIs[0].Endpoint.Path != ir.Registrations[1].Path || ir.APIs[0].Endpoint.Kind != BackendEndpointAPI || ir.APIs[0].APIName != "Health" {
+ t.Fatalf("expected API adapter metadata, got %#v", ir.APIs)
+ }
+ if len(ir.Fragments) != 1 || ir.Fragments[0].Endpoint.Path != ir.Registrations[2].Path || ir.Fragments[0].Endpoint.Kind != BackendEndpointFragment || ir.Fragments[0].Target != "#patients" {
+ t.Fatalf("expected fragment adapter metadata, got %#v", ir.Fragments)
+ }
if ir.Registrations[0].Kind != BackendEndpointAction || ir.Registrations[0].Path != "/newsletter" || ir.Registrations[0].Handler != "action" {
t.Fatalf("unexpected action registration: %#v", ir.Registrations[0])
}
if ir.Registrations[1].Kind != BackendEndpointAPI || ir.Registrations[1].Path != "/api/health" || ir.Registrations[1].Handler != "api" {
t.Fatalf("unexpected API registration: %#v", ir.Registrations[1])
}
- if ir.Registrations[2].Kind != BackendEndpointFragment || ir.Registrations[2].Path != "/patients/list" || ir.Registrations[2].Handler != "fragment" {
+ if ir.Registrations[2].Kind != BackendEndpointFragment || ir.Registrations[2].Path != "/patients/{id}/list" || ir.Registrations[2].Handler != "fragment" || !ir.Registrations[2].Dynamic {
t.Fatalf("unexpected fragment registration: %#v", ir.Registrations[2])
}
if len(ir.Decoders) != 1 || ir.Decoders[0].Function == "" || ir.Decoders[0].Input != "SubscribeInput" {
t.Fatalf("expected action decoder metadata, got %#v", ir.Decoders)
}
- if len(ir.Calls) != 2 || ir.Calls[0].Alias != "newsletter" || ir.Calls[1].Alias != "status" {
+ if len(ir.Calls) != 3 || ir.Calls[0].Alias != "newsletter" || ir.Calls[0].ImportPath != "example.com/app/newsletter" || ir.Calls[1].Alias != "status" || ir.Calls[2].Alias != "patients" {
t.Fatalf("expected bound handler calls, got %#v", ir.Calls)
}
if len(ir.Responses) != 3 || !ir.Responses[0].NoStore || ir.Responses[0].Redirect != "/newsletter?ok=1" || !ir.Responses[2].Partial {
t.Fatalf("expected no-store response metadata, got %#v", ir.Responses)
}
+ if !ir.HasEndpointKind(BackendEndpointAction) || !ir.HasEndpointKind(BackendEndpointAPI) || !ir.HasEndpointKind(BackendEndpointFragment) {
+ t.Fatalf("expected adapter endpoint kind lookup to include action, API, and fragment: %#v", ir.Registrations)
+ }
+ if !ir.HasDynamicRoutes() {
+ t.Fatalf("expected adapter IR to report dynamic fragment route")
+ }
+ guards := ir.GuardNames()
+ if len(guards) != 1 || guards[0] != "auth.required" {
+ t.Fatalf("expected adapter guard metadata, got %#v", guards)
+ }
+ imports := ir.BackendImports()
+ if imports["example.com/app/newsletter"] != "newsletter" || imports["example.com/app/status"] != "status" || imports["example.com/app/patients"] != "patients" {
+ t.Fatalf("expected adapter backend imports, got %#v", imports)
+ }
}
func TestBackendAdapterIRCapturesFallbackMetadata(t *testing.T) {
diff --git a/internal/appgen/appgen_test.go b/internal/appgen/appgen_test.go
index fd9a554..a79233b 100644
--- a/internal/appgen/appgen_test.go
+++ b/internal/appgen/appgen_test.go
@@ -593,6 +593,63 @@ func TestGenerateBackendAppRegistersBackendRoutes(t *testing.T) {
}
}
+func TestGenerateSplitFrontendProxyMatchesAdapterRoutes(t *testing.T) {
+ root := t.TempDir()
+ outputDir := filepath.Join(root, "dist")
+ appDir := filepath.Join(root, "generated-app")
+ writeTestFile(t, filepath.Join(outputDir, "index.html"), "Home")
+
+ result, err := GenerateWithOptions(outputDir, appDir, Options{
+ ProxyBackend: true,
+ Actions: []ActionEndpoint{{
+ PageID: "newsletter",
+ ActionName: "Subscribe",
+ Method: "POST",
+ Route: "/newsletter",
+ }},
+ APIs: []APIEndpoint{{
+ PageID: "status",
+ APIName: "Health",
+ Method: "GET",
+ Route: "/api/health",
+ }},
+ Fragments: []FragmentEndpoint{{
+ PageID: "patients",
+ FragmentName: "List",
+ Method: "GET",
+ Route: "/patients/{id}/list",
+ Target: "#patients",
+ HTML: "",
+ }},
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ payload, err := os.ReadFile(result.PackagePath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ source := string(payload)
+ for _, expected := range []string{
+ `Backend: backendProxy,`,
+ `func isBackendRoute(method string, requestPath string) bool`,
+ `method == http.MethodPost && requestPath == "/newsletter"`,
+ `method == "GET" && requestPath == "/api/health"`,
+ `rawRequestPath := requestPath`,
+ `gowdkroute.Match("/patients/{id}/list", rawRequestPath)`,
+ `"net/http/httputil"`,
+ `neturl "net/url"`,
+ `gowdkroute "github.com/cssbruno/gowdk/runtime/route"`,
+ } {
+ if !strings.Contains(source, expected) {
+ t.Fatalf("expected generated split frontend source to contain %q:\n%s", expected, source)
+ }
+ }
+ if strings.Contains(source, `func newBackendRouter()`) {
+ t.Fatalf("split frontend proxy should not build a local backend router:\n%s", source)
+ }
+}
+
func TestGenerateWiresCSRFWhenEnabled(t *testing.T) {
root := t.TempDir()
outputDir := filepath.Join(root, "dist")
diff --git a/internal/appgen/module.go b/internal/appgen/module.go
index c56bee9..386f67b 100644
--- a/internal/appgen/module.go
+++ b/internal/appgen/module.go
@@ -138,10 +138,11 @@ func optionsUsesModuleImports(options Options, modulePath string) bool {
// blocks.
func appBackendImportPaths(options Options) map[string]bool {
paths := map[string]bool{}
- for importPath := range backendImports(options.Actions, options.APIs, options.Fragments, options.SSR) {
+ adapter := backendAdapterIR(options)
+ for importPath := range backendImports(adapter, options.SSR) {
paths[importPath] = true
}
- for importPath := range backendContractImports(executableContractExposures(backendAdapterIR(options).ContractExposures)) {
+ for importPath := range backendContractImports(executableContractExposures(adapter.ContractExposures)) {
paths[importPath] = true
}
for importPath := range inlineGoBlockImports(options.IR) {
diff --git a/internal/appgen/source.go b/internal/appgen/source.go
index b039bdf..e29e0a6 100644
--- a/internal/appgen/source.go
+++ b/internal/appgen/source.go
@@ -41,10 +41,11 @@ func runtimeImportMap(options Options) map[string]string {
imports["os"] = "os"
imports["strings"] = "strings"
}
- actions := options.Actions
- apis := options.APIs
- fragments := options.Fragments
- contractExposures := backendAdapterIR(options).ContractExposures
+ adapter := backendAdapterIR(options)
+ actions := adapter.Actions
+ apis := adapter.APIs
+ fragments := adapter.Fragments
+ contractExposures := adapter.ContractExposures
routableContracts := routableContractExposures(contractExposures)
executableContracts := executableContractExposures(contractExposures)
if options.ProxyBackend {
@@ -95,7 +96,7 @@ func runtimeImportMap(options Options) map[string]string {
}
if options.ProxyBackend {
imports["gowdkresponse"] = "github.com/cssbruno/gowdk/runtime/response"
- if fragmentsUseDynamicRoutes(options.Fragments) {
+ if adapter.HasDynamicRoutes() {
imports["gowdkroute"] = "github.com/cssbruno/gowdk/runtime/route"
}
imports["neturl"] = "net/url"
@@ -142,7 +143,7 @@ func runtimeImportMap(options Options) map[string]string {
imports["gowdkratelimit"] = "github.com/cssbruno/gowdk/addons/ratelimit"
}
if !options.ProxyBackend {
- for importPath, alias := range backendImports(actions, apis, fragments, ssr) {
+ for importPath, alias := range backendImports(adapter, ssr) {
imports[alias] = importPath
}
for importPath, alias := range backendContractImports(executableContracts) {
@@ -249,9 +250,9 @@ func backendShellDecls(options Options) []ast.Decl {
func appGeneratedDecls(direct Options, full Options) []ast.Decl {
adapter := backendAdapterIR(direct)
- decls := actionHandlerDecls(direct.Actions, csrfEnabled(direct), generatedUsesRateLimit(direct))
- decls = append(decls, apiFuncDecl(sortedAPIEndpoints(direct.APIs), generatedUsesRateLimit(direct)))
- decls = append(decls, fragmentFuncDecl(direct.Fragments, generatedUsesRateLimit(direct)))
+ decls := actionHandlerDecls(adapter.Actions, csrfEnabled(direct), generatedUsesRateLimit(direct))
+ decls = append(decls, apiFuncDecl(adapter.APIs, generatedUsesRateLimit(direct)))
+ decls = append(decls, fragmentFuncDecl(adapter.Fragments, generatedUsesRateLimit(direct)))
decls = append(decls, contractHandlerDecls(adapter.ContractExposures, csrfEnabled(direct), generatedUsesRateLimit(direct))...)
decls = append(decls, contractDecoderDecls(adapter.ContractExposures)...)
decls = append(decls, contractEventSinkDecls(adapter.ContractExposures)...)
@@ -263,7 +264,7 @@ func appGeneratedDecls(direct Options, full Options) []ast.Decl {
decls = append(decls, emptyBackendHandlerDecl())
}
if full.ProxyBackend {
- decls = append(decls, backendProxyDecl(generatedUsesRateLimit(full)), isBackendRouteDecl(full))
+ decls = append(decls, backendProxyDecl(generatedUsesRateLimit(full)), isBackendRouteDecl(backendAdapterIR(full)))
}
if csrfEnabled(direct) {
decls = append(decls, csrfValidatorVarDecl(), csrfNewFuncDecl(direct.Config.Build.CSRF))
@@ -276,9 +277,9 @@ func appGeneratedDecls(direct Options, full Options) []ast.Decl {
func backendGeneratedDecls(options Options) []ast.Decl {
adapter := backendAdapterIR(options)
- decls := actionHandlerDecls(options.Actions, csrfEnabled(options), generatedUsesRateLimit(options))
- decls = append(decls, apiFuncDecl(sortedAPIEndpoints(options.APIs), generatedUsesRateLimit(options)))
- decls = append(decls, fragmentFuncDecl(options.Fragments, generatedUsesRateLimit(options)))
+ decls := actionHandlerDecls(adapter.Actions, csrfEnabled(options), generatedUsesRateLimit(options))
+ decls = append(decls, apiFuncDecl(adapter.APIs, generatedUsesRateLimit(options)))
+ decls = append(decls, fragmentFuncDecl(adapter.Fragments, generatedUsesRateLimit(options)))
decls = append(decls, contractHandlerDecls(adapter.ContractExposures, csrfEnabled(options), generatedUsesRateLimit(options))...)
decls = append(decls, contractDecoderDecls(adapter.ContractExposures)...)
decls = append(decls, contractEventSinkDecls(adapter.ContractExposures)...)
@@ -296,8 +297,8 @@ func backendGeneratedDecls(options Options) []ast.Decl {
return decls
}
-func actionHandlerDecls(actions []ActionEndpoint, csrf bool, rateLimit bool) []ast.Decl {
- sorted := sortedActionEndpoints(actions)
+func actionHandlerDecls(actions []BackendActionAdapter, csrf bool, rateLimit bool) []ast.Decl {
+ sorted := sortedActionAdapters(actions)
decls := []ast.Decl{actionFuncDecl(sorted, csrf, rateLimit)}
if len(sorted) > 0 {
decls = append(decls, actionRequestPathDecl())
@@ -532,13 +533,14 @@ func errorPagesExpr(options Options) ast.Expr {
}
func customErrorPagePaths(options Options) []string {
+ adapter := backendAdapterIR(options)
seen := map[string]bool{}
- for _, action := range options.Actions {
+ for _, action := range adapter.Actions {
if action.ErrorPage != "" {
seen[action.ErrorPage] = true
}
}
- for _, api := range options.APIs {
+ for _, api := range adapter.APIs {
if api.ErrorPage != "" {
seen[api.ErrorPage] = true
}
@@ -601,7 +603,8 @@ func importSpecSource(imports map[string]string) string {
}
func csrfEnabled(options Options) bool {
- return options.Config.Build.CSRF.Enabled && (len(options.Actions) > 0 || contractExposuresParseForm(executableContractExposures(backendAdapterIR(options).ContractExposures)))
+ adapter := backendAdapterIR(options)
+ return options.Config.Build.CSRF.Enabled && (adapter.HasEndpointKind(BackendEndpointAction) || contractExposuresParseForm(executableContractExposures(adapter.ContractExposures)))
}
func csrfHelperSource(options Options) (string, error) {
@@ -666,26 +669,11 @@ func backendCallbackName(options Options) string {
}
func hasBackendRoutes(options Options) bool {
- return len(options.Actions) > 0 || len(options.APIs) > 0 || len(options.Fragments) > 0 || hasRoutableContractReferences(options)
+ return backendAdapterIR(options).HasRegistrations()
}
-func backendImports(actions []ActionEndpoint, apis []APIEndpoint, fragments []FragmentEndpoint, ssr []SSRRoute) map[string]string {
- imports := map[string]string{}
- for _, action := range actions {
- if action.Binding.ImportPath != "" && action.BackendAlias != "" {
- imports[action.Binding.ImportPath] = action.BackendAlias
- }
- }
- for _, api := range apis {
- if api.Binding.ImportPath != "" && api.BackendAlias != "" {
- imports[api.Binding.ImportPath] = api.BackendAlias
- }
- }
- for _, fragment := range fragments {
- if fragment.Binding.ImportPath != "" && fragment.BackendAlias != "" {
- imports[fragment.Binding.ImportPath] = fragment.BackendAlias
- }
- }
+func backendImports(adapter BackendAdapterIR, ssr []SSRRoute) map[string]string {
+ imports := adapter.BackendImports()
for _, route := range ssr {
// Guardless routes are denied before rendering, so their load handler is
// never called and contributes no import. Importing it anyway leaves an
diff --git a/internal/appgen/source_actions.go b/internal/appgen/source_actions.go
index 6d82e95..29181b1 100644
--- a/internal/appgen/source_actions.go
+++ b/internal/appgen/source_actions.go
@@ -15,7 +15,7 @@ import (
)
func actionHandlerSource(actions []ActionEndpoint, csrf bool) (string, error) {
- sorted := sortedActionEndpoints(actions)
+ sorted := backendAdapterIR(Options{Actions: actions}).Actions
decls := []ast.Decl{actionFuncDecl(sorted, csrf, false)}
if len(sorted) > 0 {
decls = append(decls, actionRequestPathDecl())
@@ -38,7 +38,7 @@ func printActionDecls(decls []ast.Decl) (string, error) {
return formatGoDeclSnippet(buffer.String())
}
-func actionsUseValidation(actions []ActionEndpoint) bool {
+func actionsUseValidation(actions []BackendActionAdapter) bool {
for _, action := range actions {
if actionsUseActionValidation(action) {
return true
@@ -47,7 +47,7 @@ func actionsUseValidation(actions []ActionEndpoint) bool {
return false
}
-func actionsUseLengthValidation(actions []ActionEndpoint) bool {
+func actionsUseLengthValidation(actions []BackendActionAdapter) bool {
for _, action := range actions {
if !actionsUseActionValidation(action) {
continue
@@ -61,11 +61,11 @@ func actionsUseLengthValidation(actions []ActionEndpoint) bool {
return false
}
-func actionsUseActionValidation(action ActionEndpoint) bool {
+func actionsUseActionValidation(action BackendActionAdapter) bool {
return action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature && action.ValidatesInput
}
-func actionsUseForm(actions []ActionEndpoint) bool {
+func actionsUseForm(actions []BackendActionAdapter) bool {
for _, action := range actions {
if action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature && actionNeedsValues(action) {
return true
@@ -74,7 +74,7 @@ func actionsUseForm(actions []ActionEndpoint) bool {
return false
}
-func actionsUseFragments(actions []ActionEndpoint) bool {
+func actionsUseFragments(actions []BackendActionAdapter) bool {
for _, action := range actions {
if actionUsesPartialAddon(action) {
return true
@@ -83,11 +83,11 @@ func actionsUseFragments(actions []ActionEndpoint) bool {
return false
}
-func actionUsesPartialAddon(action ActionEndpoint) bool {
+func actionUsesPartialAddon(action BackendActionAdapter) bool {
return action.Binding.Status == "" && len(action.Fragments) > 0
}
-func actionsParseForm(actions []ActionEndpoint) bool {
+func actionsParseForm(actions []BackendActionAdapter) bool {
for _, action := range actions {
if action.Binding.Status != source.BackendBindingMissing && action.Binding.Status != source.BackendBindingUnsupportedSignature {
return true
@@ -107,7 +107,18 @@ func sortedActionEndpoints(actions []ActionEndpoint) []ActionEndpoint {
return sorted
}
-func actionNeedsValues(action ActionEndpoint) bool {
+func sortedActionAdapters(actions []BackendActionAdapter) []BackendActionAdapter {
+ sorted := append([]BackendActionAdapter(nil), actions...)
+ sort.Slice(sorted, func(i, j int) bool {
+ if sorted[i].Route == sorted[j].Route {
+ return sorted[i].ActionName < sorted[j].ActionName
+ }
+ return sorted[i].Route < sorted[j].Route
+ })
+ return sorted
+}
+
+func actionNeedsValues(action BackendActionAdapter) bool {
if action.Binding.Status != source.BackendBindingBound {
return true
}
@@ -117,7 +128,7 @@ func actionNeedsValues(action ActionEndpoint) bool {
return action.Binding.Signature != source.BackendSignatureAction0
}
-func actionFuncDecl(actions []ActionEndpoint, csrf bool, rateLimit bool) *ast.FuncDecl {
+func actionFuncDecl(actions []BackendActionAdapter, csrf bool, rateLimit bool) *ast.FuncDecl {
if len(actions) == 0 {
return funcDecl("action", actionParams(), boolResults(), []ast.Stmt{returnBool(false)})
}
@@ -152,8 +163,8 @@ func actionRequestPathDecl() *ast.FuncDecl {
})
}
-func actionCaseStmts(action ActionEndpoint, csrf bool, rateLimit bool) []ast.Stmt {
- stmts := endpointContextStmts("action", action.PageID, action.ActionName, actionMethod(action), action.Route, action.ErrorPage)
+func actionCaseStmts(action BackendActionAdapter, csrf bool, rateLimit bool) []ast.Stmt {
+ stmts := endpointContextStmts("action", action.PageID, action.ActionName, actionAdapterMethod(action), action.Route, action.ErrorPage)
if action.ErrorPage != "" {
stmts = append(stmts, endpointPanicBoundaryStmt())
}
@@ -226,7 +237,7 @@ func actionParseFormStmtsWithErrors(csrf bool, jsonErrors bool) []ast.Stmt {
return stmts
}
-func actionInputDecodeStmts(action ActionEndpoint) []ast.Stmt {
+func actionInputDecodeStmts(action BackendActionAdapter) []ast.Stmt {
if action.Binding.Status == source.BackendBindingBound {
return boundActionInputDecodeStmts(action)
}
@@ -245,7 +256,7 @@ func actionInputDecodeStmts(action ActionEndpoint) []ast.Stmt {
return stmts
}
-func boundActionInputDecodeStmts(action ActionEndpoint) []ast.Stmt {
+func boundActionInputDecodeStmts(action BackendActionAdapter) []ast.Stmt {
switch action.Binding.Signature {
case source.BackendSignatureAction0:
if action.ValidatesInput {
@@ -273,7 +284,7 @@ func boundActionInputDecodeStmts(action ActionEndpoint) []ast.Stmt {
}
}
-func expectedValuesStmts(action ActionEndpoint) []ast.Stmt {
+func expectedValuesStmts(action BackendActionAdapter) []ast.Stmt {
return []ast.Stmt{&ast.IfStmt{
Init: define([]ast.Expr{id("decodedValues"), id("err")}, call(sel("gowdkform", "DecodeExpected"), id("values"), formSchemaExpr(action.InputFields))),
Cond: notNil("err"),
@@ -285,7 +296,7 @@ func expectedValuesStmts(action ActionEndpoint) []ast.Stmt {
}}
}
-func actionRequiredValidationStmts(action ActionEndpoint) []ast.Stmt {
+func actionRequiredValidationStmts(action BackendActionAdapter) []ast.Stmt {
stmts := []ast.Stmt{
define([]ast.Expr{id("validation")}, &ast.CompositeLit{Type: sel("gowdkvalidation", "Result")}),
}
@@ -389,7 +400,7 @@ func actionValidationMessage(custom string, fallback string) string {
return custom
}
-func boundActionResultStmts(action ActionEndpoint) []ast.Stmt {
+func boundActionResultStmts(action BackendActionAdapter) []ast.Stmt {
args := []ast.Expr{id("ctx")}
switch action.Binding.Signature {
case source.BackendSignatureAction0:
@@ -421,6 +432,14 @@ func actionMethod(action ActionEndpoint) string {
return method
}
+func actionAdapterMethod(action BackendActionAdapter) string {
+ method := strings.ToUpper(strings.TrimSpace(action.Method))
+ if method == "" {
+ return "POST"
+ }
+ return method
+}
+
func endpointContextStmt(kind, pageID, name, method, route, errorPage string) ast.Stmt {
return define(
[]ast.Expr{id("ctx")},
@@ -456,7 +475,7 @@ func endpointMetadataExpr(kind, pageID, name, method, route, errorPage string) a
}
}
-func actionsUseErrorPages(actions []ActionEndpoint) bool {
+func actionsUseErrorPages(actions []BackendActionAdapter) bool {
for _, action := range actions {
if action.ErrorPage != "" {
return true
@@ -479,7 +498,7 @@ func endpointPanicBoundaryStmt() ast.Stmt {
})}
}
-func actionPartialBranchStmts(action ActionEndpoint) []ast.Stmt {
+func actionPartialBranchStmts(action BackendActionAdapter) []ast.Stmt {
body := []ast.Stmt{}
if len(action.Fragments) == 0 {
body = append(body,
@@ -545,7 +564,7 @@ func fragmentResponseStmts(fragment ActionFragment) []ast.Stmt {
}
}
-func actionResultStmts(action ActionEndpoint) []ast.Stmt {
+func actionResultStmts(action BackendActionAdapter) []ast.Stmt {
if strings.TrimSpace(action.Redirect) == "" {
return []ast.Stmt{
setNoStoreHeaderStmt(),
@@ -555,7 +574,7 @@ func actionResultStmts(action ActionEndpoint) []ast.Stmt {
return []ast.Stmt{writeNoStoreHTTPStmt(call(sel("gowdkresponse", "RedirectTo"), stringLit(action.Redirect)))}
}
-func actionDecoderDecls(actions []ActionEndpoint) []ast.Decl {
+func actionDecoderDecls(actions []BackendActionAdapter) []ast.Decl {
var decls []ast.Decl
for _, inputType := range uniqueInputTypes(actions) {
decls = append(decls, &ast.GenDecl{
@@ -584,7 +603,7 @@ func actionDecoderDecls(actions []ActionEndpoint) []ast.Decl {
return decls
}
-func valuesActionDecoderDecl(action ActionEndpoint) *ast.FuncDecl {
+func valuesActionDecoderDecl(action BackendActionAdapter) *ast.FuncDecl {
inputType := action.InputType
return funcDecl(actionDecoderName(action), []*ast.Field{
{Names: []*ast.Ident{id("values")}, Type: sel("gowdkform", "Values")},
@@ -607,7 +626,7 @@ func valuesActionDecoderDecl(action ActionEndpoint) *ast.FuncDecl {
})
}
-func uniqueInputTypes(actions []ActionEndpoint) []string {
+func uniqueInputTypes(actions []BackendActionAdapter) []string {
seen := map[string]bool{}
var types []string
for _, action := range actions {
@@ -621,14 +640,14 @@ func uniqueInputTypes(actions []ActionEndpoint) []string {
return types
}
-func actionUsesBoundInputDecoder(action ActionEndpoint) bool {
+func actionUsesBoundInputDecoder(action BackendActionAdapter) bool {
if action.Binding.Status != source.BackendBindingBound {
return false
}
return action.Binding.Signature == source.BackendSignatureActionForm || action.Binding.Signature == source.BackendSignatureActionFormPtr
}
-func boundActionDecoderDecl(action ActionEndpoint) *ast.FuncDecl {
+func boundActionDecoderDecl(action BackendActionAdapter) *ast.FuncDecl {
inputType := sel(action.BackendAlias, action.Binding.InputType)
stmts := []ast.Stmt{
define([]ast.Expr{id("input")}, &ast.CompositeLit{Type: inputType}),
@@ -706,11 +725,11 @@ func convertIfNeeded(goType string, value ast.Expr) ast.Expr {
return call(id(goType), value)
}
-func actionDecoderName(action ActionEndpoint) string {
+func actionDecoderName(action BackendActionAdapter) string {
return "decode" + source.ExportedIdentifier(action.PageID, "Action") + source.ExportedIdentifier(action.ActionName, "Action") + "Input"
}
-func boundActionDecoderName(action ActionEndpoint) string {
+func boundActionDecoderName(action BackendActionAdapter) string {
return "decode" + source.ExportedIdentifier(action.PageID, "Action") + source.ExportedIdentifier(action.ActionName, "Action") + "BoundInput"
}
diff --git a/internal/appgen/source_api.go b/internal/appgen/source_api.go
index eb05a00..c6f4823 100644
--- a/internal/appgen/source_api.go
+++ b/internal/appgen/source_api.go
@@ -8,10 +8,10 @@ import (
)
func apiHandlerSource(apis []APIEndpoint) (string, error) {
- return printActionDecls([]ast.Decl{apiFuncDecl(sortedAPIEndpoints(apis), false)})
+ return printActionDecls([]ast.Decl{apiFuncDecl(backendAdapterIR(Options{APIs: apis}).APIs, false)})
}
-func apiFuncDecl(apis []APIEndpoint, rateLimit bool) *ast.FuncDecl {
+func apiFuncDecl(apis []BackendAPIAdapter, rateLimit bool) *ast.FuncDecl {
if len(apis) == 0 {
return funcDecl("api", actionParams(), boolResults(), []ast.Stmt{returnBool(false)})
}
@@ -39,7 +39,7 @@ func apiFuncDecl(apis []APIEndpoint, rateLimit bool) *ast.FuncDecl {
})
}
-func apiCaseExpr(api APIEndpoint) ast.Expr {
+func apiCaseExpr(api BackendAPIAdapter) ast.Expr {
return &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: selExpr(id("request"), "Method"),
@@ -55,7 +55,7 @@ func apiCaseExpr(api APIEndpoint) ast.Expr {
}
}
-func apiCaseStmts(api APIEndpoint, rateLimit bool) []ast.Stmt {
+func apiCaseStmts(api BackendAPIAdapter, rateLimit bool) []ast.Stmt {
stmts := endpointContextStmts("api", api.PageID, api.APIName, api.Method, api.Route, api.ErrorPage)
if api.ErrorPage != "" {
stmts = append(stmts, endpointPanicBoundaryStmt())
@@ -83,7 +83,7 @@ func apiCaseStmts(api APIEndpoint, rateLimit bool) []ast.Stmt {
return stmts
}
-func apisUseErrorPages(apis []APIEndpoint) bool {
+func apisUseErrorPages(apis []BackendAPIAdapter) bool {
for _, api := range apis {
if api.ErrorPage != "" {
return true
diff --git a/internal/appgen/source_backend.go b/internal/appgen/source_backend.go
index f239dea..55e60e2 100644
--- a/internal/appgen/source_backend.go
+++ b/internal/appgen/source_backend.go
@@ -9,13 +9,7 @@ import (
func newBackendRouterDecl(adapter BackendAdapterIR) *ast.FuncDecl {
routes := []ast.Expr{}
for _, registration := range adapter.Registrations {
- var method ast.Expr = stringLit(registration.Method)
- if registration.Kind == BackendEndpointAction && registration.Method == "POST" {
- method = sel("http", "MethodPost")
- } else if registration.Kind == BackendEndpointFragment && registration.Method == "GET" {
- method = sel("http", "MethodGet")
- }
- routes = append(routes, backendRouteExpr(method, registration.Kind, registration.Path, id(registration.Handler)))
+ routes = append(routes, backendRouteExpr(backendRegistrationMethodExpr(registration), registration.Kind, registration.Path, id(registration.Handler)))
}
for _, exposure := range routableContractExposures(adapter.ContractExposures) {
method := contractExposureMethodExpr(exposure)
@@ -34,6 +28,17 @@ func newBackendRouterDecl(adapter BackendAdapterIR) *ast.FuncDecl {
}, stmts)
}
+func backendRegistrationMethodExpr(registration BackendEndpointRegistration) ast.Expr {
+ switch {
+ case registration.Kind == BackendEndpointAction && registration.Method == "POST":
+ return sel("http", "MethodPost")
+ case registration.Kind == BackendEndpointFragment && registration.Method == "GET":
+ return sel("http", "MethodGet")
+ default:
+ return stringLit(registration.Method)
+ }
+}
+
func contractRouteHandlerExpr(exposure BackendContractExposure) ast.Expr {
handler := sel(contractHandlerName(exposure))
if contractExposureExecutable(exposure) {
@@ -93,7 +98,7 @@ func backendProxySource(options Options) (string, error) {
}
return printActionDecls([]ast.Decl{
backendProxyDecl(false),
- isBackendRouteDecl(options),
+ isBackendRouteDecl(backendAdapterIR(options)),
})
}
@@ -137,48 +142,45 @@ func backendProxyDecl(rateLimit bool) *ast.FuncDecl {
return funcDecl("backendProxy", actionParams(), boolResults(), stmts)
}
-func isBackendRouteDecl(options Options) *ast.FuncDecl {
+func isBackendRouteDecl(adapter BackendAdapterIR) *ast.FuncDecl {
clauses := []ast.Stmt{}
- for _, action := range sortedActionEndpoints(options.Actions) {
- clauses = append(clauses, &ast.CaseClause{
- List: []ast.Expr{backendRouteCond(sel("http", "MethodPost"), action.Route)},
- Body: []ast.Stmt{returnBool(true)},
- })
- }
- for _, api := range sortedAPIEndpoints(options.APIs) {
- clauses = append(clauses, &ast.CaseClause{
- List: []ast.Expr{backendRouteCond(stringLit(api.Method), api.Route)},
- Body: []ast.Stmt{returnBool(true)},
- })
- }
- for _, fragment := range sortedFragmentEndpoints(options.Fragments) {
- if fragmentRouteIsDynamic(fragment) {
+ for _, registration := range adapter.Registrations {
+ if registration.Dynamic {
continue
}
clauses = append(clauses, &ast.CaseClause{
- List: []ast.Expr{backendRouteCond(stringLit(fragment.Method), fragment.Route)},
+ List: []ast.Expr{backendRouteCond(backendRegistrationMethodExpr(registration), registration.Path)},
Body: []ast.Stmt{returnBool(true)},
})
}
- for _, exposure := range routableContractExposures(backendAdapterIR(options).ContractExposures) {
+ for _, exposure := range routableContractExposures(adapter.ContractExposures) {
+ if exposure.Endpoint.Dynamic {
+ continue
+ }
clauses = append(clauses, &ast.CaseClause{
List: []ast.Expr{backendRouteCond(contractExposureMethodExpr(exposure), exposure.Endpoint.Path)},
Body: []ast.Stmt{returnBool(true)},
})
}
body := []ast.Stmt{}
- if fragmentsUseDynamicRoutes(options.Fragments) {
+ if adapter.HasDynamicRoutes() {
body = append(body, define([]ast.Expr{id("rawRequestPath")}, id("requestPath")))
}
body = append(body,
assign([]ast.Expr{id("requestPath")}, call(sel("path", "Clean"), &ast.BinaryExpr{X: stringLit("/"), Op: token.ADD, Y: id("requestPath")})),
&ast.SwitchStmt{Body: &ast.BlockStmt{List: clauses}},
)
- for _, fragment := range sortedFragmentEndpoints(options.Fragments) {
- if !fragmentRouteIsDynamic(fragment) {
+ for _, registration := range adapter.Registrations {
+ if !registration.Dynamic {
+ continue
+ }
+ body = append(body, backendDynamicRouteIfStmt(backendRegistrationMethodExpr(registration), registration.Path))
+ }
+ for _, exposure := range routableContractExposures(adapter.ContractExposures) {
+ if !exposure.Endpoint.Dynamic {
continue
}
- body = append(body, backendDynamicRouteIfStmt(stringLit(fragment.Method), fragment.Route))
+ body = append(body, backendDynamicRouteIfStmt(contractExposureMethodExpr(exposure), exposure.Endpoint.Path))
}
body = append(body, returnBool(false))
return funcDecl("isBackendRoute", []*ast.Field{
diff --git a/internal/appgen/source_backend_app.go b/internal/appgen/source_backend_app.go
index af9a08e..bfa23d1 100644
--- a/internal/appgen/source_backend_app.go
+++ b/internal/appgen/source_backend_app.go
@@ -12,13 +12,14 @@ func backendRuntimeImportSource(options Options) string {
func backendRuntimeImportMap(options Options) map[string]string {
imports := map[string]string{}
- contractExposures := backendAdapterIR(options).ContractExposures
+ adapter := backendAdapterIR(options)
+ contractExposures := adapter.ContractExposures
routableContracts := routableContractExposures(contractExposures)
executableContracts := executableContractExposures(contractExposures)
if hasBackendRoutes(options) {
imports["gowdkruntime"] = "github.com/cssbruno/gowdk/runtime/app"
}
- if len(options.Actions) > 0 || len(options.APIs) > 0 || len(options.Fragments) > 0 || len(routableContracts) > 0 {
+ if len(adapter.Registrations) > 0 || len(routableContracts) > 0 {
imports["gowdkresponse"] = "github.com/cssbruno/gowdk/runtime/response"
}
if len(executableContracts) > 0 {
@@ -34,28 +35,28 @@ func backendRuntimeImportMap(options Options) map[string]string {
if contractExposuresParseForm(executableContracts) {
imports["strings"] = "strings"
}
- if len(options.Actions) > 0 {
+ if len(adapter.Actions) > 0 {
imports["path"] = "path"
}
- if actionsParseForm(options.Actions) {
+ if actionsParseForm(adapter.Actions) {
imports["strings"] = "strings"
}
- if actionsUseForm(options.Actions) {
+ if actionsUseForm(adapter.Actions) {
imports["gowdkform"] = "github.com/cssbruno/gowdk/runtime/form"
}
- if len(options.APIs) > 0 {
+ if len(adapter.APIs) > 0 {
imports["path"] = "path"
}
- if fragmentsUseExactRoutes(options.Fragments) {
+ if fragmentsUseExactRoutes(adapter.Fragments) {
imports["path"] = "path"
}
- if fragmentsUseDynamicRoutes(options.Fragments) {
+ if fragmentsUseDynamicRoutes(adapter.Fragments) {
imports["gowdkroute"] = "github.com/cssbruno/gowdk/runtime/route"
}
- if actionsUseFragments(options.Actions) || fragmentsUseStaticFallback(options.Fragments) {
+ if actionsUseFragments(adapter.Actions) || fragmentsUseStaticFallback(adapter.Fragments) {
imports["gowdkpartial"] = "github.com/cssbruno/gowdk/addons/partial"
}
- if actionsUseValidation(options.Actions) {
+ if actionsUseValidation(adapter.Actions) {
imports["gowdkvalidation"] = "github.com/cssbruno/gowdk/runtime/validation"
}
if generatedUsesGuards(options) {
@@ -68,7 +69,7 @@ func backendRuntimeImportMap(options Options) map[string]string {
imports["os"] = "os"
imports["strings"] = "strings"
}
- for importPath, alias := range backendImports(options.Actions, options.APIs, options.Fragments, nil) {
+ for importPath, alias := range backendImports(adapter, nil) {
imports[alias] = importPath
}
for importPath, alias := range backendContractImports(executableContracts) {
diff --git a/internal/appgen/source_contracts.go b/internal/appgen/source_contracts.go
index 5525358..8c07974 100644
--- a/internal/appgen/source_contracts.go
+++ b/internal/appgen/source_contracts.go
@@ -405,13 +405,6 @@ func backendContractImports(exposures []BackendContractExposure) map[string]stri
return imports
}
-func hasRoutableContractReferences(options Options) bool {
- if options.IR == nil {
- return false
- }
- return len(routableContractExposures(backendAdapterIR(options).ContractExposures)) > 0
-}
-
func contractHandlerName(exposure BackendContractExposure) string {
return string(exposure.Endpoint.Kind) +
source.ExportedIdentifier(exposure.OwnerID, "Contract") +
diff --git a/internal/appgen/source_fragments.go b/internal/appgen/source_fragments.go
index a2db710..158fd04 100644
--- a/internal/appgen/source_fragments.go
+++ b/internal/appgen/source_fragments.go
@@ -9,11 +9,11 @@ import (
"github.com/cssbruno/gowdk/internal/source"
)
-func fragmentFuncDecl(fragments []FragmentEndpoint, rateLimit bool) *ast.FuncDecl {
+func fragmentFuncDecl(fragments []BackendFragmentAdapter, rateLimit bool) *ast.FuncDecl {
if len(fragments) == 0 {
return funcDecl("fragment", actionParams(), boolResults(), []ast.Stmt{returnBool(false)})
}
- sorted := sortedFragmentEndpoints(fragments)
+ sorted := sortedFragmentAdapters(fragments)
body := []ast.Stmt{}
var clauses []ast.Stmt
for _, fragment := range sorted {
@@ -45,7 +45,7 @@ func fragmentFuncDecl(fragments []FragmentEndpoint, rateLimit bool) *ast.FuncDec
return funcDecl("fragment", actionParams(), boolResults(), body)
}
-func fragmentCaseExpr(fragment FragmentEndpoint) ast.Expr {
+func fragmentCaseExpr(fragment BackendFragmentAdapter) ast.Expr {
return &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: selExpr(id("request"), "Method"),
@@ -61,7 +61,7 @@ func fragmentCaseExpr(fragment FragmentEndpoint) ast.Expr {
}
}
-func fragmentCaseStmts(fragment FragmentEndpoint, rateLimit bool) []ast.Stmt {
+func fragmentCaseStmts(fragment BackendFragmentAdapter, rateLimit bool) []ast.Stmt {
stmts := fragmentContextStmts(fragment, false)
stmts = append(stmts, rateLimitStmts(rateLimit)...)
stmts = append(stmts, guardStmts(fragment.Guards)...)
@@ -93,7 +93,7 @@ func fragmentCaseStmts(fragment FragmentEndpoint, rateLimit bool) []ast.Stmt {
return stmts
}
-func fragmentDynamicIfStmt(fragment FragmentEndpoint, rateLimit bool) ast.Stmt {
+func fragmentDynamicIfStmt(fragment BackendFragmentAdapter, rateLimit bool) ast.Stmt {
return &ast.IfStmt{
Init: define([]ast.Expr{id("params"), id("ok")}, call(sel("gowdkroute", "Match"), stringLit(fragment.Route), selExpr(selExpr(id("request"), "URL"), "Path"))),
Cond: &ast.BinaryExpr{
@@ -109,7 +109,7 @@ func fragmentDynamicIfStmt(fragment FragmentEndpoint, rateLimit bool) ast.Stmt {
}
}
-func fragmentDynamicCaseStmts(fragment FragmentEndpoint, rateLimit bool) []ast.Stmt {
+func fragmentDynamicCaseStmts(fragment BackendFragmentAdapter, rateLimit bool) []ast.Stmt {
stmts := fragmentContextStmts(fragment, true)
stmts = append(stmts, rateLimitStmts(rateLimit)...)
stmts = append(stmts, guardStmts(fragment.Guards)...)
@@ -141,7 +141,7 @@ func fragmentDynamicCaseStmts(fragment FragmentEndpoint, rateLimit bool) []ast.S
return stmts
}
-func fragmentContextStmts(fragment FragmentEndpoint, includeParams bool) []ast.Stmt {
+func fragmentContextStmts(fragment BackendFragmentAdapter, includeParams bool) []ast.Stmt {
stmts := []ast.Stmt{
endpointContextStmt("fragment", fragment.PageID, fragment.FragmentName, fragment.Method, fragment.Route, ""),
}
@@ -153,14 +153,14 @@ func fragmentContextStmts(fragment FragmentEndpoint, includeParams bool) []ast.S
return stmts
}
-func fragmentTypedRouteParams(fragment FragmentEndpoint) []source.RouteParam {
+func fragmentTypedRouteParams(fragment BackendFragmentAdapter) []source.RouteParam {
if len(fragment.RouteParams) > 0 {
return fragment.RouteParams
}
return gwdkir.RouteParamsFromPath(fragment.Route)
}
-func fragmentsUseStaticFallback(fragments []FragmentEndpoint) bool {
+func fragmentsUseStaticFallback(fragments []BackendFragmentAdapter) bool {
for _, fragment := range fragments {
if fragment.Binding.Status != source.BackendBindingBound && fragment.Binding.Status != source.BackendBindingUnsupportedSignature {
return true
@@ -169,7 +169,7 @@ func fragmentsUseStaticFallback(fragments []FragmentEndpoint) bool {
return false
}
-func fragmentsUseExactRoutes(fragments []FragmentEndpoint) bool {
+func fragmentsUseExactRoutes(fragments []BackendFragmentAdapter) bool {
for _, fragment := range fragments {
if !fragmentRouteIsDynamic(fragment) {
return true
@@ -178,7 +178,7 @@ func fragmentsUseExactRoutes(fragments []FragmentEndpoint) bool {
return false
}
-func fragmentsUseDynamicRoutes(fragments []FragmentEndpoint) bool {
+func fragmentsUseDynamicRoutes(fragments []BackendFragmentAdapter) bool {
for _, fragment := range fragments {
if fragmentRouteIsDynamic(fragment) {
return true
@@ -187,7 +187,7 @@ func fragmentsUseDynamicRoutes(fragments []FragmentEndpoint) bool {
return false
}
-func fragmentRouteIsDynamic(fragment FragmentEndpoint) bool {
+func fragmentRouteIsDynamic(fragment BackendFragmentAdapter) bool {
return len(ssrRoutePatternParams(fragment.Route)) > 0
}
@@ -204,3 +204,17 @@ func sortedFragmentEndpoints(fragments []FragmentEndpoint) []FragmentEndpoint {
})
return sorted
}
+
+func sortedFragmentAdapters(fragments []BackendFragmentAdapter) []BackendFragmentAdapter {
+ sorted := append([]BackendFragmentAdapter(nil), fragments...)
+ sort.Slice(sorted, func(i, j int) bool {
+ if sorted[i].Route == sorted[j].Route {
+ if sorted[i].Method == sorted[j].Method {
+ return sorted[i].FragmentName < sorted[j].FragmentName
+ }
+ return sorted[i].Method < sorted[j].Method
+ }
+ return sorted[i].Route < sorted[j].Route
+ })
+ return sorted
+}
diff --git a/internal/appgen/source_guards.go b/internal/appgen/source_guards.go
index ee12d67..f5e95af 100644
--- a/internal/appgen/source_guards.go
+++ b/internal/appgen/source_guards.go
@@ -8,10 +8,7 @@ import (
)
func generatedUsesGuards(options Options) bool {
- if endpointsUseRuntimeGuards(options.Actions, options.APIs, options.Fragments) {
- return true
- }
- if contractExposuresUseRuntimeGuards(backendAdapterIR(options).ContractExposures) {
+ if adapterUsesRuntimeGuards(backendAdapterIR(options)) {
return true
}
for _, route := range options.SSR {
@@ -22,32 +19,8 @@ func generatedUsesGuards(options Options) bool {
return false
}
-func endpointsUseRuntimeGuards(actions []ActionEndpoint, apis []APIEndpoint, fragments []FragmentEndpoint) bool {
- for _, action := range actions {
- if len(runtimeGuardNames(action.Guards)) > 0 {
- return true
- }
- }
- for _, api := range apis {
- if len(runtimeGuardNames(api.Guards)) > 0 {
- return true
- }
- }
- for _, fragment := range fragments {
- if len(runtimeGuardNames(fragment.Guards)) > 0 {
- return true
- }
- }
- return false
-}
-
-func contractExposuresUseRuntimeGuards(exposures []BackendContractExposure) bool {
- for _, exposure := range routableContractExposures(exposures) {
- if len(runtimeGuardNames(exposure.Guards)) > 0 {
- return true
- }
- }
- return false
+func adapterUsesRuntimeGuards(adapter BackendAdapterIR) bool {
+ return len(runtimeGuardNames(adapter.GuardNames())) > 0
}
func guardDecls(options Options) []ast.Decl {
@@ -170,19 +143,7 @@ func generatedUsesNativeRBACGuards(options Options) bool {
}
func generatedGuardNames(options Options) []string {
- var guards []string
- for _, action := range options.Actions {
- guards = append(guards, action.Guards...)
- }
- for _, api := range options.APIs {
- guards = append(guards, api.Guards...)
- }
- for _, fragment := range options.Fragments {
- guards = append(guards, fragment.Guards...)
- }
- for _, exposure := range routableContractExposures(backendAdapterIR(options).ContractExposures) {
- guards = append(guards, exposure.Guards...)
- }
+ guards := backendAdapterIR(options).GuardNames()
for _, route := range options.SSR {
guards = append(guards, route.Guards...)
}
diff --git a/internal/appgen/source_rate_limit.go b/internal/appgen/source_rate_limit.go
index 3488c24..592bf8b 100644
--- a/internal/appgen/source_rate_limit.go
+++ b/internal/appgen/source_rate_limit.go
@@ -8,11 +8,9 @@ import (
)
func generatedUsesRateLimit(options Options) bool {
+ adapter := backendAdapterIR(options)
return options.Config.HasFeature(gowdk.FeatureRateLimit) &&
- (len(options.Actions) > 0 ||
- len(options.APIs) > 0 ||
- len(options.Fragments) > 0 ||
- len(routableContractExposures(backendAdapterIR(options).ContractExposures)) > 0 ||
+ (adapter.HasRegistrations() ||
len(options.SSR) > 0)
}