vernac / architecture, expressed.
Browse documentation

Inbound MVC adapter contract

Status: 0.5.0 development. This contract extends the existing inbound HTTP boundary with server-rendered views; it does not introduce a UI/template language.

Declaration and ownership

adapter Name inbound mvc uses Usecase ... { ... } declares an internal HTTP entrypoint. Like JSON inbound adapters, it generates a Spring controller and scoped advice in <namespace>.adapter.inbound.http. Usecases own transactions and business behavior. The controller MUST NOT create a transaction. Thymeleaf/templates, CSS, JavaScript, authentication and CSRF configuration remain application-owned.

An MVC adapter MUST declare an absolute base path and a safe fallback error view "problem";. HTTP route collision detection is shared with JSON adapters, including placeholders. Activated modules register MVC entrypoints/advice using the existing inbound component mechanism; entrypoints are not exported callable types. The host application must supply its template engine and template resources.

Requests and usecase arguments

An endpoint declares GET or POST, path/query/header/form scalar bindings, an execute dependency { argument <- group.field; } block, then a view or redirect. The same exact scalar/VO conversions and named mappings as JSON inbound apply. There is no implicit normalization, conversion by name, repository access, or SQL.

Form names and mapping paths are explicit. An absent optional field and an empty String are distinct. Optional domain values remain governed by ordinary Vernac contracts. File uploads, repeated collection fields, multipart bodies and arbitrary Java request objects are outside this first contract.

Successful responses

render "board" model tasks; passes a non-optional usecase result as attribute tasks, status 200 by default. The result is the complete returned value, implicit Result object or completed page; it is not flattened into untyped attributes. render "thanks"; is allowed for a void result. A returned value MUST have an explicit model name; a void result MUST NOT claim one. A render status MAY be an explicit 2xx other than 204/205. inputs, error and notice are reserved model names.

An optional result requires on absent render "missing" status 404; (404 or 410). It is unwrapped only at this top-level boundary. Nested Optional values keep their ordinary Java API. A successful redirect "/"; discards any returned value explicitly. It uses 303 after POST and 302 after GET. Redirect targets MUST be static, application-local paths; external/protocol-relative targets and model propagation into URLs are forbidden. Optional message "Saved." supplies a static flash notice. Dynamic redirects are deferred rather than built from guessed string substitutions.

Templates are logical slash-separated names, not expressions, absolute files, redirect/forward instructions or traversal paths. The template engine resolves them; the compiler does not assume a template root or inspect template expressions.

GET endpoints and redisplay loaders MUST be designed for reading. The compiler cannot prove arbitrary Java usecase bodies free of side effects. A committed usecase is not rolled back if subsequent view rendering fails.

Error policies and redisplay

Endpoint rules precede adapter rules, followed by safe defaults. Rules use existing selectors: invalid request/cursor, validation, not found, optimistic conflict, domain rejection, typed rejection code, and integration responsibility/category. Codes remain typed enum constants where the existing HTTP contract requires them. Only the public code/message selected by a rule or safe defaults are exposed. Exception messages, causes, stack traces and infrastructure names are not exposed implicitly. errors implemented by retains the HTTP static Reply contract and uses the adapter’s fallback error view.

then render "conflict" status 409 "CODE" "Public message"; selects a dedicated error page. then respond 409 ...; uses the fallback error view with that status; it never switches an MVC endpoint to JSON. Statuses MUST be 4xx/5xx.

then redisplay board status 422 "INVALID_TITLE" "Check the task title."; reloads the named GET endpoint’s view model and then renders its view with the chosen error status. This is a page-loader reference, not a template name. It MUST target a GET endpoint with a direct, non-optional render result in the same adapter. Every required binding needed by the loader MUST be available on the failed endpoint under the same kind/external name as a required binding of the same wire type, or have a loader default. No implicit usecase input is invented. Redisplay is endpoint-specific and MUST NOT be declared as an adapter-wide rule. It cannot recursively redisplay or rerun the failed POST usecase.

A loader uses its declared inputs/defaults; original allowed form values are then restored into the public inputs model. Sensitive/header values are not restored. The original POST’s HTTP status is replaced with the rule’s error status. If the loader fails, the operation MUST end at the safe fallback error view with status 500; no recursive policy or optimistic retry is attempted. The original transaction has already ended at the failed usecase boundary. Redisplay is a new usecase call; it does not undo prior commits or external effects.

Safe defaults follow the JSON boundary: malformed/missing input 400, domain validation/rejection 422, not found 404, optimistic conflict 409 and unknown technical failure 500. Technical integration status customization uses the same explicit policies, for example remote TIMEOUT → 503. Technical failures are logged; the boundary itself MUST NOT log input maps. Custom exceptions/logging remain the application author’s responsibility.

Every error view receives error (safe code/message) and inputs (allowed raw strings). This first version supports a form-wide public message; it MUST NOT pretend to know which form field caused an arbitrary domain exception. Structured field errors may be added later. Escaping is the template author’s responsibility; the supplied Thymeleaf starter uses escaped text/values, never th:utext for input.

Routing/content negotiation failures and security filters may reject a request before the controller is selected. They keep the framework’s HTTP behavior; MVC policies are not a substitute for authentication, authorization or CSRF handling.

Tooling and verification

The LSP MUST provide semantic diagnostics, dependency/argument/binding completion, and navigation for redisplay endpoint references. Native type navigation and Java mapping injection remain available. Shared keyword highlighting MUST recognize MVC syntax. Source maps for copied Java mappings keep their existing contract; native view-template editing/debugging remains the host framework’s responsibility.

Tests MUST cover actual Spring MVC/Thymeleaf rendering, input preservation, sensitive/undeclared data exclusion, malformed/duplicate values, optional absence, local redirects, precedence, loader failures, technical errors and CSRF integration, alongside compiler diagnostics and existing JSON inbound regressions.

External implementation targets also follow the 0.5.0 shared Java implementation resolution rules. Their mapping/error-handler calling convention is unchanged.