yesod-core 1.6.14 → 1.7.0.0
raw patch · 151 files changed
Files
- ChangeLog.md +187/−0
- bench/widget.hs +5/−6
- docs/split-route-compilation.md +256/−0
- src/Yesod/Core.hs +21/−2
- src/Yesod/Core/Class/Breadcrumbs.hs +8/−4
- src/Yesod/Core/Class/Dispatch.hs +200/−7
- src/Yesod/Core/Class/Dispatch/ToParentRoute.hs +18/−0
- src/Yesod/Core/Class/Handler.hs +10/−3
- src/Yesod/Core/Class/Yesod.hs +39/−12
- src/Yesod/Core/Content.hs +15/−2
- src/Yesod/Core/Dispatch.hs +140/−46
- src/Yesod/Core/Handler.hs +86/−41
- src/Yesod/Core/Internal/LiteApp.hs +3/−7
- src/Yesod/Core/Internal/Request.hs +15/−6
- src/Yesod/Core/Internal/Response.hs +1/−2
- src/Yesod/Core/Internal/Run.hs +43/−36
- src/Yesod/Core/Internal/TH.hs +410/−140
- src/Yesod/Core/Internal/Util.hs +0/−1
- src/Yesod/Core/Json.hs +16/−4
- src/Yesod/Core/TypeCache.hs +0/−0
- src/Yesod/Core/Types.hs +61/−88
- src/Yesod/Core/Types/Content.hs +21/−0
- src/Yesod/Core/Types/ErrorResponse.hs +37/−0
- src/Yesod/Core/Types/HandlerContents.hs +39/−0
- src/Yesod/Core/Types/TypedContent.hs +101/−0
- src/Yesod/Core/Widget.hs +149/−24
- src/Yesod/Routes/Class.hs +98/−1
- src/Yesod/Routes/Overlap.hs +10/−2
- src/Yesod/Routes/Parse.hs +128/−84
- src/Yesod/Routes/TH.hs +31/−2
- src/Yesod/Routes/TH/Dispatch.hs +1054/−202
- src/Yesod/Routes/TH/Internal.hs +412/−0
- src/Yesod/Routes/TH/ParseRoute.hs +321/−34
- src/Yesod/Routes/TH/RenderRoute.hs +979/−167
- src/Yesod/Routes/TH/RouteAttrs.hs +93/−17
- src/Yesod/Routes/TH/Types.hs +106/−36
- test/Hierarchy.hs +150/−166
- test/Hierarchy/Admin.hs +29/−0
- test/Hierarchy/Nest.hs +29/−0
- test/Hierarchy/Nest2.hs +28/−0
- test/Hierarchy/Nest2/NestInner.hs +23/−0
- test/Hierarchy/Nest3.hs +16/−0
- test/Hierarchy/ResourceTree.hs +54/−0
- test/Route/DeepAritySpec.hs +61/−0
- test/Route/DeepArityTypes.hs +15/−0
- test/Route/DiscoveryModeSpec.hs +49/−0
- test/Route/FallthroughSpec.hs +108/−0
- test/Route/FocusLeafConsSpec.hs +73/−0
- test/Route/InlineParseClausesSpec.hs +213/−0
- test/Route/InstanceProbeSpec.hs +151/−0
- test/Route/InstanceProbeTypes.hs +116/−0
- test/Route/MissingFocusTargetSpec.hs +83/−0
- test/Route/MissingFocusTargetTypes.hs +29/−0
- test/Route/NestedParseClausesSpec.hs +61/−0
- test/Route/PureQ.hs +34/−0
- test/Route/RenderRouteSpec.hs +47/−0
- test/Route/RouteAttrSpec.hs +54/−0
- test/Route/SubDispatchAritySpec.hs +78/−0
- test/RouteSpec.hs +79/−173
- test/YesodCoreTest.hs +59/−3
- test/YesodCoreTest/Auth.hs +6/−1
- test/YesodCoreTest/BangSeparatorRuntime.hs +51/−0
- test/YesodCoreTest/Breadcrumb.hs +56/−0
- test/YesodCoreTest/Cache.hs +6/−5
- test/YesodCoreTest/CleanPath.hs +8/−5
- test/YesodCoreTest/Content.hs +79/−0
- test/YesodCoreTest/Csrf.hs +6/−3
- test/YesodCoreTest/ErrorHandling.hs +88/−4
- test/YesodCoreTest/ErrorHandling/CustomApp.hs +43/−0
- test/YesodCoreTest/Exceptions.hs +7/−3
- test/YesodCoreTest/FallthroughDispatch/Resources.hs +36/−0
- test/YesodCoreTest/FallthroughDispatch/Runtime.hs +74/−0
- test/YesodCoreTest/FallthroughMatrix/FirstFoo.hs +32/−0
- test/YesodCoreTest/FallthroughMatrix/Resources.hs +40/−0
- test/YesodCoreTest/FallthroughMatrix/Runtime.hs +100/−0
- test/YesodCoreTest/Header.hs +6/−2
- test/YesodCoreTest/InternalRequest.hs +4/−4
- test/YesodCoreTest/JsAttributes.hs +40/−0
- test/YesodCoreTest/JsLoader.hs +6/−2
- test/YesodCoreTest/JsLoaderSites/Bottom.hs +6/−2
- test/YesodCoreTest/Json.hs +7/−1
- test/YesodCoreTest/Links.hs +7/−2
- test/YesodCoreTest/LiteApp.hs +1/−0
- test/YesodCoreTest/Media.hs +7/−2
- test/YesodCoreTest/MediaData.hs +6/−2
- test/YesodCoreTest/Meta.hs +55/−0
- test/YesodCoreTest/MultiPieceNestedRuntime.hs +57/−0
- test/YesodCoreTest/NestedDispatch/InnerR.hs +29/−0
- test/YesodCoreTest/NestedDispatch/NestR.hs +27/−0
- test/YesodCoreTest/NestedDispatch/Parent0R.hs +36/−0
- test/YesodCoreTest/NestedDispatch/Parent0R/Child0R.hs +31/−0
- test/YesodCoreTest/NestedDispatch/ParentR.hs +30/−0
- test/YesodCoreTest/NestedDispatch/Resources.hs +59/−0
- test/YesodCoreTest/NestedDispatch/Runtime.hs +599/−0
- test/YesodCoreTest/NoOverloadedStrings.hs +9/−4
- test/YesodCoreTest/NoOverloadedStringsSub.hs +6/−2
- test/YesodCoreTest/ParamDefaultSplit/Data.hs +28/−0
- test/YesodCoreTest/ParamDefaultSplit/Runtime.hs +88/−0
- test/YesodCoreTest/ParamFallthroughRuntime.hs +85/−0
- test/YesodCoreTest/ParamFocusSplit/Resources.hs +24/−0
- test/YesodCoreTest/ParamFocusSplit/Runtime.hs +92/−0
- test/YesodCoreTest/ParamFocusSplit/SubR.hs +35/−0
- test/YesodCoreTest/ParamNestedNoFallthroughRuntime.hs +75/−0
- test/YesodCoreTest/ParamNoExplicitArgs.hs +86/−0
- test/YesodCoreTest/ParamNoFallthroughRuntime.hs +77/−0
- test/YesodCoreTest/ParamSubsite/Data.hs +45/−0
- test/YesodCoreTest/ParamSubsite/InstanceRuntime.hs +118/−0
- test/YesodCoreTest/ParamSubsite/SplitNested.hs +65/−0
- test/YesodCoreTest/ParamSubsite/SplitRuntime.hs +113/−0
- test/YesodCoreTest/ParamTopLevelRuntime.hs +96/−0
- test/YesodCoreTest/ParameterizedSite.hs +55/−0
- test/YesodCoreTest/ParameterizedSite/Compat.hs +31/−0
- test/YesodCoreTest/ParameterizedSite/PolyAny.hs +30/−0
- test/YesodCoreTest/ParameterizedSite/PolyShow.hs +32/−0
- test/YesodCoreTest/ParameterizedSite/SubRoute.hs +80/−0
- test/YesodCoreTest/ParameterizedSubData.hs +50/−0
- test/YesodCoreTest/ParameterizedSubDispatch.hs +67/−0
- test/YesodCoreTest/ParameterizedSubDispatch/Data.hs +27/−0
- test/YesodCoreTest/ParameterizedSubDispatchRuntime.hs +181/−0
- test/YesodCoreTest/ParameterizedSubDispatchRuntime/Data.hs +44/−0
- test/YesodCoreTest/RawResponse.hs +7/−1
- test/YesodCoreTest/Redirect.hs +6/−1
- test/YesodCoreTest/RenderRouteSpec.hs +51/−0
- test/YesodCoreTest/RenderRouteSpec/TH.hs +32/−0
- test/YesodCoreTest/Reps.hs +7/−1
- test/YesodCoreTest/RequestBodySize.hs +6/−2
- test/YesodCoreTest/RuntimeHarness.hs +83/−0
- test/YesodCoreTest/SplitSubsite/Data.hs +24/−0
- test/YesodCoreTest/SplitSubsite/NestedR.hs +42/−0
- test/YesodCoreTest/SplitSubsite/Runtime.hs +62/−0
- test/YesodCoreTest/Ssl.hs +7/−2
- test/YesodCoreTest/Streaming.hs +1/−0
- test/YesodCoreTest/StubLaxSameSite.hs +6/−1
- test/YesodCoreTest/StubSslOnly.hs +6/−1
- test/YesodCoreTest/StubStrictSameSite.hs +6/−1
- test/YesodCoreTest/StubUnsecured.hs +6/−1
- test/YesodCoreTest/SubSub.hs +51/−0
- test/YesodCoreTest/SubSubData.hs +20/−0
- test/YesodCoreTest/SubsiteFallthrough/Data.hs +40/−0
- test/YesodCoreTest/SubsiteFallthrough/Nested.hs +59/−0
- test/YesodCoreTest/SubsiteFallthrough/Runtime.hs +63/−0
- test/YesodCoreTest/SubsiteOptsFallthrough/Data.hs +41/−0
- test/YesodCoreTest/SubsiteOptsFallthrough/Runtime.hs +87/−0
- test/YesodCoreTest/WaiSubsite.hs +8/−1
- test/YesodCoreTest/Widget.hs +8/−3
- test/YesodCoreTest/ZeroPieceShadow/FallApp.hs +35/−0
- test/YesodCoreTest/ZeroPieceShadow/ShadowApp.hs +36/−0
- test/YesodCoreTest/ZeroPieceShadowRuntime.hs +43/−0
- test/fixtures/routes_with_line_continuations +0/−11
- test/fixtures/routes_with_line_continuations.yesodroutes +11/−0
- yesod-core.cabal +191/−24
ChangeLog.md view
@@ -1,5 +1,192 @@ # ChangeLog for yesod-core +## 1.7.0.0++* Split route compilation ([#1887](https://github.com/yesodweb/yesod/pull/1887), [guide](https://github.com/yesodweb/yesod/blob/master/yesod-core/docs/split-route-compilation.md)):+ * Use `setFocusOnNestedRoute` to generate a nested route block's types and dispatch in their own module — for top-level sites, subsites, and parameterized foundations (`data App a`).+ * `setNestedRouteFallthrough` lets a nested parent with no matching child fall through to later sibling routes instead of committing to a 404.+ * Nested route fragments work in `redirect`/`setUrl`: wrap them in `WithParentArgs`, or use the bare constructor when the parent binds no dynamic pieces.+ * `mkNestedSubDispatchInstance` (for hand-written split subsite modules) takes resources as `[ResourceTree String]` — the `parseRoutes` quasi-quoter's output — and parses the type strings internally. No `map (fmap (parseType . dropBracket))` step, and a malformed type fails the splice with an attributable error.+ * Clearer TH errors for malformed route blocks, subsite/nested-datatype arity mismatches, and missing focus targets.+ * Breaking: modules that splice a nested route block need `MultiParamTypeClasses` (and usually `FlexibleContexts`).+ * Breaking: TH codegen entry points changed (`TyArgs` threading; `mkDispatchClause`, `mkParseRouteInstance`, `mkRouteConsOpts`, `mkDispatchInstance`); `mkRenderRouteClauses` and the `MkRouteOpts` constructor are no longer exported.+* `ResourceParent` records the parent's own route attributes; `frParentDetails` replaces `frParentPieces`. [#1911](https://github.com/yesodweb/yesod/pull/1911)++## 1.6.29.1++* Fix compilation error for text >= 2.1.2 [#1905](https://github.com/yesodweb/yesod/pull/1905)++## 1.6.29.0++* When showing HandlerContent, include a content snippet [#1864](https://github.com/yesodweb/yesod/pull/1864)++## 1.6.28.1++* Add type arguments to the sub routes [#1866](https://github.com/yesodweb/yesod/pull/1866)++## 1.6.28.0 ++* Allow users to control generation of the `resourcesSite :: [ResourceTree String]` value. [#1881](https://github.com/yesodweb/yesod/pull/1881)++## 1.6.27.1++* Set `base >= 4.11` for less CPP and imports [#1876](https://github.com/yesodweb/yesod/pull/1876)++## 1.6.27.0++* Build with `wai-extra-3.1.17` [#1861](https://github.com/yesodweb/yesod/pull/1861)++## 1.6.26.0++* Always apply jsAttributes to julius script blocks of body [#1836](https://github.com/yesodweb/yesod/pull/1836)++## 1.6.25.1++* Export the options that were created in 1.6.25.0 [#1825](https://github.com/yesodweb/yesod/pull/1825)++## 1.6.25.0++* Add an options structure that allows the user to set which instances will be derived for a routes structure. [#1819](https://github.com/yesodweb/yesod/pull/1819)++## 1.6.24.5++* Support Aeson 2.2 [#1818](https://github.com/yesodweb/yesod/pull/1818)++## 1.6.24.4++* Fix test-suite compilation error for GHC >= 9.0.1 [#1812](https://github.com/yesodweb/yesod/pull/1812)++## 1.6.24.3++* Fix subsite-to-subsite dispatch [#1805](https://github.com/yesodweb/yesod/pull/1805)++## 1.6.24.2++* No star is type [#1797](https://github.com/yesodweb/yesod/pull/1797)++## 1.6.24.1++* Adapt to removal of `ListT` from transformers-0.6. [#1796](https://github.com/yesodweb/yesod/pull/1796)++## 1.6.24.0++* Make catching exceptions configurable and set the default back to rethrowing async exceptions. [#1772](https://github.com/yesodweb/yesod/pull/1772).++## 1.6.23.1++* Fix typo in creation of the description `<meta>` tag in `defaultLayout`. [#1766](https://github.com/yesodweb/yesod/pull/1766)++## 1.6.23++* Add idempotent versions of `setDescription`, `setDescriptionI`. These functions+ have odd behaviour when called multiple times, so they are now warned against.+ This can't be a silent change - if you want to switch to the new functions, make+ sure your layouts are updated to use `pageDescription` as well as `pageTitle`.+ [#1765](https://github.com/yesodweb/yesod/pull/1765)++## 1.6.22.1+++ Remove sometimes failing superfluous test. [#1756](https://github.com/yesodweb/yesod/pull/1756)++## 1.6.22.0++* Add missing list to documentation for ``Yesod.Core.Dispatch.warp``. [#1745](https://github.com/yesodweb/yesod/pull/1745)+* Add instances for `ToContent Void`, `ToTypedContent Void`. [#1752](https://github.com/yesodweb/yesod/pull/1752)+* Handle async exceptions within yesod rather then warp. [#1753](https://github.com/yesodweb/yesod/pull/1753)+* Support template-haskell 2.18 [#1754](https://github.com/yesodweb/yesod/pull/1754)++## 1.6.21.0++* Export `Yesod.Core.Dispatch.defaultGen` so that users may reuse it for their own `YesodRunnerEnv`s [#1734](https://github.com/yesodweb/yesod/pull/1734)++## 1.6.20.2++* Fix compatibility with template-haskell 2.17 [#1729](https://github.com/yesodweb/yesod/pull/1729)++## 1.6.20.1++* Throw an error in `breadcrumbs` if the trail of breadcrumbs is circular. [#1727](https://github.com/yesodweb/yesod/issues/1727)++## 1.6.20++* Generate CSRF tokens using a secure entropy source [#1726](https://github.com/yesodweb/yesod/pull/1726)+* Change semantics of `yreGen` and `defaultGen`++## 1.6.19.0++* Change order of priority in `languages`[#1721](https://github.com/yesodweb/yesod/pull/1721)++## 1.6.18.8++* Fix test suite for wai-extra change around vary header++## 1.6.18.7++* Fix functions generating Open Graph metadata[#1709](https://github.com/yesodweb/yesod/pull/1709)++## 1.6.18.6++* Update documentation from `HandlerT` to `HandlerFor` [#1703](https://github.com/yesodweb/yesod/pull/1703)++## 1.6.18.5++Document `ErrorResponse` [#1698](https://github.com/yesodweb/yesod/pull/1698)++## 1.6.18.4++* Fixed a bug where `mkYesod` and other TH functions didn't work for datatypes with explicitly stated type variables, including the case with typeclass constraints. [https://github.com/yesodweb/yesod/pull/1697](#1697)++## 1.6.18.3++* Remove mention of an oudated Yesod type (`GHandler`) from the docs for `handlerToIO`. [https://github.com/yesodweb/yesod/pull/1695](#1695)++## 1.6.18.2++* Recommends `.yesodroutes` as the file extension for Yesod routes files. [#1686](https://github.com/yesodweb/yesod/pull/1686)++## 1.6.18.1++* Increase the size of CSRF token++## 1.6.18++* Add functions for setting description and OG meta [#1663](https://github.com/yesodweb/yesod/pull/1663)++* Use `DeriveLift` to implement the `Lift` instances for `ResourceTree`,+ `Resource`, `Piece`, and `Dispatch`. Among other benefits, this provides+ implementations of `liftTyped` on `template-haskell-2.16` (GHC 8.10) or+ later. [#1664](https://github.com/yesodweb/yesod/pull/1664)++## 1.6.17.3++* Support for `unliftio-core` 0.2++## 1.6.17.2++* Support template-haskell 2.16, build with GHC 8.10 [#1657](https://github.com/yesodweb/yesod/pull/1657)++## 1.6.17.1++* Remove unnecessary deriving of Typeable++## 1.6.17++* Adds `contentTypeIsJson` [#1646](https://github.com/yesodweb/yesod/pull/1646)++## 1.6.16.1++* Compiles with GHC 8.8.1++## 1.6.16++* Add `jsAttributesHandler` to run arbitrary Handler code before building the+ attributes map for the script tag generated by `widgetFile` [#1622](https://github.com/yesodweb/yesod/pull/1622)++## 1.6.15++* Move `redirectToPost` JavaScript form submission from HTML element to+ `<script>` tag for CSP reasons [#1620](https://github.com/yesodweb/yesod/pull/1620)+ ## 1.6.14 * Introduce `JSONResponse`. [issue #1481](https://github.com/yesodweb/yesod/issues/1481) and [PR #1592](https://github.com/yesodweb/yesod/pull/1592)
bench/widget.hs view
@@ -1,15 +1,14 @@-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-}--- | BigTable benchmark implemented using Hamlet.---+{-# LANGUAGE OverloadedStrings #-}+-- BigTable benchmark implemented using Hamlet. {-# LANGUAGE QuasiQuotes #-}+ module Main where import Gauge.Main import Text.Hamlet import qualified Data.ByteString.Lazy as L import qualified Text.Blaze.Html.Renderer.Utf8 as Utf8-import Data.Monoid (mconcat) import Text.Blaze.Html5 (table, tr, td) import Text.Blaze.Html (toHtml) import Data.Int@@ -64,6 +63,6 @@ -} bigTableBlaze :: Show a => [[a]] -> Int64-bigTableBlaze t = L.length $ Utf8.renderHtml $ table $ Data.Monoid.mconcat $ map row t+bigTableBlaze t = L.length $ Utf8.renderHtml $ table $ foldMap row t where- row r = tr $ mconcat $ map (td . toHtml . show) r+ row r = tr $ foldMap (td . toHtml . show) r
+ docs/split-route-compilation.md view
@@ -0,0 +1,256 @@+# Split route compilation++As of yesod-core 1.7, route definitions can be split across multiple modules.+A nested route block gets its own datatype, instances, and dispatch generated+in its own module, and the main `mkYesod` splice *delegates* to that module+instead of regenerating everything in one place.++Why you'd want this:++* **Smaller TH splices.** A large site no longer needs one giant `mkYesod`+ splice that regenerates everything whenever any route changes.+* **Parallel compilation.** Each fragment module compiles independently.+* **Locality.** A route group's handlers, datatype, and dispatch live in one+ module, next to each other.+* **Faster test feedback.** Each fragment gets its own `YesodDispatchNested`+ instance, which is enough to dispatch a request on its own.+ [hspec-yesod](https://github.com/parsonsmatt/hspec-yesod) is building on+ this to run specs against a single route fragment, so a spec depends only on+ the handlers it actually exercises — editing an unrelated handler no longer+ recompiles (or relinks) the test module.++## How it works++Any nested route block — a parent declared with a trailing `:` — gets its own+route datatype:++```+/nest NestR:+ / NestIndexR GET POST+ /#Int NestShowR GET POST+```++By default, `mkYesod` generates the `NestR` datatype and its dispatch inline,+exactly as in 1.6. But if a `YesodDispatchNested NestR` instance is already in+scope at the splice site (because a separately compiled module generated it),+`mkYesod` delegates to that instance instead. Single-module sites are+unchanged; splitting is opt-in and per-parent.++## Recipe: top-level site++Three modules: a shared route table, the split-out fragment, and the main site.++First, put the route definitions in their own module so both sides can see+them — along with your project's `RouteOpts`. Every splice that touches the+route table should use the same options, so define them once (see+[Fallthrough](#fallthrough) for why fallthrough should be on):++```haskell+module App.Routes.Resources where++import Yesod.Core++appRouteOpts :: RouteOpts+appRouteOpts = setNestedRouteFallthrough True defaultOpts++appRouteOptsFor :: String -> RouteOpts+appRouteOptsFor name = setFocusOnNestedRoute name appRouteOpts++appResources :: [ResourceTree String]+appResources = [parseRoutes|+ / HomeR GET++ /nest NestR:+ / NestIndexR GET POST+ /#Int NestShowR GET POST+|]+```++Then generate the `NestR` fragment in its own module by focusing the splice+on it. The fragment's handlers live here too:++```haskell+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}++module App.Routes.NestR where++import Yesod.Core+import Data.Text (Text)+import App.Routes.Resources++mkYesodOpts (appRouteOptsFor "NestR") "App" appResources++getNestIndexR :: HandlerFor App Text+getNestIndexR = pure "nest index"++postNestIndexR :: HandlerFor App Text+postNestIndexR = pure "posted"++getNestShowR :: Int -> HandlerFor App Text+getNestShowR _ = pure "nest show"++postNestShowR :: Int -> HandlerFor App Text+postNestShowR _ = pure "posted"+```++Finally, the main module imports the fragment and splices with the same+options:++```haskell+module App where++import Yesod.Core+import App.Routes.Resources+import App.Routes.NestR (NestR (..))++mkYesodOpts appRouteOpts "App" appResources++getHomeR :: HandlerFor App Text+getHomeR = pure "home"+```++Because `App.Routes.NestR` compiled first, the `mkYesodOpts` splice sees the+`YesodDispatchNested NestR` instance and delegates to it rather than+regenerating the `NestR` code. If you want to separate the datatype from the+dispatch further, `mkYesodDataOpts` and `mkYesodDispatchOpts` accept the same+options.++## Recipe: subsites++Subsites split the same way. The data module generates the subsite's route+datatype and nested-fragment instances:++```haskell+module App.SplitSub.Data where++import Yesod.Core++data SplitSub = SplitSub++mkYesodSubData "SplitSub" [parseRoutes|+/ SplitHomeR GET+/nested NestedR:+ / NestedHomeR GET+ /detail/#Int NestedDetailR GET+|]+```++The split-out module holds the nested handlers and generates the+`YesodSubDispatchNested` instance with `mkNestedSubDispatchInstance`:++```haskell+{-# OPTIONS_GHC -Wno-orphans #-}++module App.SplitSub.NestedR (NestedR (..)) where++import Yesod.Core+import Yesod.Core.Dispatch+ (mkNestedSubDispatchInstance, defaultOpts, TyArgs (..))+import App.SplitSub.Data++getNestedHomeR :: SubHandlerFor SplitSub master Text+getNestedHomeR = pure "nested home"++getNestedDetailR :: Int -> SubHandlerFor SplitSub master Text+getNestedDetailR _ = pure "nested detail"++$(mkNestedSubDispatchInstance+ defaultOpts -- or your project-wide RouteOpts; see Fallthrough below+ "NestedR"+ [] -- no instance context+ NoTyArgs -- no type arguments (non-parameterized subsite)+ return -- handler unwrapper+ resourcesSplitSub) -- the [ResourceTree String] from your routes file+```++`mkNestedSubDispatchInstance` takes the resources as `[ResourceTree String]` —+exactly what the `parseRoutes` quasi-quoter produces — and parses the type+strings internally, failing the splice with an attributable error on a+malformed type rather than a deferred runtime `error`.++And the subsite's `YesodSubDispatch` instance delegates automatically, as long+as the split-out instance is in scope:++```haskell+import App.SplitSub.Data+import App.SplitSub.NestedR () -- instance import only; no handlers leak++instance YesodSubDispatch SplitSub master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesSplitSub)+```++For a subsite defined entirely in one module (including parameterized subsites+like `data MySub a`), `mkYesodSubDispatchInstance "(MyClass a) => MySub a"+resourcesMySub` generates the `YesodSubDispatch` and nested instances in one+splice.++## Linking to nested routes++A nested fragment constructor isn't a `Route App` on its own — its parent may+bind dynamic pieces the fragment doesn't carry. To use a fragment in+`redirect` or `setUrl`, wrap it in `WithParentArgs` together with the parent's+dynamic arguments:++```haskell+redirect (WithParentArgs userId (ProfileEditR "name"))+```++When the parent binds no dynamic pieces, the bare constructor works directly —+generated `RedirectUrl`/`UrlToDispatch` instances fill in the empty parent+arguments:++```haskell+redirect NestIndexR+```++For this reason, if you are splitting up your routes for compilation, it is+recommended to avoid captures in the parent prefix (prefer+`/admin AdminR:` over `/user/#UserId UserR:`). With a static-only prefix,+every fragment constructor is usable directly in `redirect`/`setUrl`, and+`WithParentArgs` never enters the picture. Dynamic pieces can still live on+the individual child routes.++To convert a fragment into its parent's route type explicitly, use+`toParentRoute` from `Yesod.Core.Class.Dispatch.ToParentRoute` (not+re-exported from `Yesod.Core`, since the name is easy to collide with).++## Fallthrough++By default, once dispatch enters a nested parent whose subtree has no matching+child, the response is a 404 — later sibling routes are never tried. This+matters a lot when splitting: the usual first step is wrapping a batch of+previously-flat routes in a new nested group, and with fallthrough off, any+request matching the group's prefix now *commits* to that group. A route+declared later that shares the prefix silently becomes unreachable — flat+dispatch would have kept trying, the grouped version 404s. Enabling+`setNestedRouteFallthrough` restores the flat-dispatch behavior: a parent+whose subtree has no match falls through to the routes after it.++Fallthrough is decided per splice: each module containing a parent route+decides for its own parents. Mixing modules spliced with different options+gives confusingly inconsistent dispatch, which is why the recipe above defines+`appRouteOpts`/`appRouteOptsFor` once and uses them everywhere — don't reach+for `defaultOpts` directly in individual modules.++Related gotcha: a nested parent with *no* leading static path piece matches+unconditionally, so siblings declared after it are unreachable unless+fallthrough is enabled. Give the parent a static segment or enable+fallthrough.++## Troubleshooting++* **`Target 'NestR' was not found in resources.`** — the name passed to+ `setFocusOnNestedRoute` doesn't match any nested parent in the route table.+* **Missing-extension errors** — fragment-generating modules need+ `MultiParamTypeClasses`, and usually `FlexibleContexts` and+ `FlexibleInstances`. GHC's error names the missing extension.+* **Duplicate `RedirectUrl` instance** — the generated convenience instance is+ `OVERLAPPABLE`, so a *more specific* hand-written instance wins, but a+ hand-written instance with the identical head is still a duplicate-instance+ error.
src/Yesod/Core.hs view
@@ -1,16 +1,30 @@-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}+ module Yesod.Core ( -- * Type classes Yesod (..) , YesodDispatch (..)+ , YesodDispatchNested (..) , YesodSubDispatch (..)+ , YesodSubDispatchNested (..) , RenderRoute (..)+ -- | Note: the 'toParentRoute' method is intentionally /not/ re-exported+ -- here. It collides with a common local binding name (e.g. the+ -- @toParentRoute@ from @getRouteToParent@), so an @import Yesod@ should not+ -- inject it. Import it from "Yesod.Core.Class.Dispatch.ToParentRoute" when+ -- you need it.+ , ToParentRoute+ , RenderRouteNested (..) , ParseRoute (..) , RouteAttrs (..)+ -- | @since 1.7.0.0+ , ParseRouteNested (..)+ -- | @since 1.7.0.0+ , RouteAttrsNested (..) -- ** Breadcrumbs , YesodBreadcrumbs (..) , breadcrumbs@@ -18,6 +32,10 @@ , Approot (..) , FileUpload (..) , ErrorResponse (..)+ -- | The field accessors @theParentArgs@\/@parentArgsFor@ are not+ -- re-exported (generic names that would shadow common local bindings);+ -- only the type and its constructor are.+ , WithParentArgs (WithParentArgs) -- * Utilities , maybeAuthorized , widgetToPageContent@@ -140,6 +158,7 @@ import Data.Version (showVersion) import Yesod.Routes.Class import UnliftIO (MonadIO (..), MonadUnliftIO (..))+import Yesod.Core.Class.Dispatch.ToParentRoute import Control.Monad.Trans.Resource (MonadResource (..)) import Yesod.Core.Internal.LiteApp
src/Yesod/Core/Class/Breadcrumbs.hs view
@@ -1,4 +1,6 @@+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Core.Class.Breadcrumbs where import Yesod.Core.Handler@@ -15,7 +17,7 @@ -- | Gets the title of the current page and the hierarchy of parent pages, -- along with their respective titles.-breadcrumbs :: YesodBreadcrumbs site => HandlerFor site (Text, [(Route site, Text)])+breadcrumbs :: (YesodBreadcrumbs site, Show (Route site), Eq (Route site)) => HandlerFor site (Text, [(Route site, Text)]) breadcrumbs = do x <- getCurrentRoute case x of@@ -26,6 +28,8 @@ return (title, z) where go back Nothing = return back- go back (Just this) = do- (title, next) <- breadcrumb this- go ((this, title) : back) next+ go back (Just this)+ | this `elem` map fst back = error $ "yesod-core: infinite recursion in breadcrumbs at " ++ show this+ | otherwise = do+ (title, next) <- breadcrumb this+ go ((this, title) : back) next
src/Yesod/Core/Class/Dispatch.hs view
@@ -1,25 +1,171 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+ module Yesod.Core.Class.Dispatch where -import qualified Network.Wai as W-import Yesod.Core.Types-import Yesod.Core.Content (ToTypedContent (..))-import Yesod.Core.Handler (sendWaiApplication)+import Data.ByteString.Builder (toLazyByteString, byteString)+import Data.Proxy (Proxy(..))+import Data.Text (Text)+import Network.HTTP.Types (status301, status307)+import Yesod.Core.Class.Dispatch.ToParentRoute import Yesod.Core.Class.Yesod+import Yesod.Core.Content (ToTypedContent (..))+import Yesod.Core.Handler (sendWaiApplication, RedirectUrl, notFound)+import Yesod.Core.Internal.Run+import Yesod.Core.Types+import Yesod.Routes.Class+import qualified Data.ByteString as S+import qualified Data.ByteString.Lazy as BL+import qualified Network.Wai as W -- | This class is automatically instantiated when you use the template haskell -- mkYesod function. You should never need to deal with it directly. class Yesod site => YesodDispatch site where yesodDispatch :: YesodRunnerEnv site -> W.Application +-- | This class enables you to dispatch on a route fragment without needing+-- to know how to dispatch on the entire route structure. This allows you+-- to break up route generation into multiple files.+--+-- For details on use, see 'setFocusOnNestedRoute'.+--+-- @since 1.7.0.0+class RenderRouteNested a => YesodDispatchNested a where+ -- | Dispatches a request to a nested route fragment.+ --+ -- The implementation uses the full WAI 'Request' to determine if the+ -- remaining path (after the parent route) matches any child routes.+ -- Returns 'Nothing' if no child routes match (for fallthrough to other+ -- routes), or 'Just' a continuation that handles the request.+ --+ -- The parent depth (number of path pieces consumed by the parent route)+ -- is statically known during Template Haskell generation and baked into+ -- the generated instance.+ --+ -- @since 1.7.0.0+ yesodDispatchNested+ :: (Yesod (ParentSite a))+ => Proxy a+ -- ^ Type proxy to resolve ambiguity from non-injective type families+ -> ParentArgs a+ -- ^ The dynamic arguments from the parent route+ -> (a -> Route (ParentSite a))+ -- ^ Function to wrap the nested route in the parent constructor+ -> YesodRunnerEnv (ParentSite a)+ -- ^ The runner environment+ -> W.Request+ -- ^ The full WAI request+ -> Maybe ((W.Response -> IO W.ResponseReceived) -> IO W.ResponseReceived)+ -- ^ Returns 'Nothing' for fallthrough, or 'Just' a continuation+ -- that completes the 'Application' type when given a respond callback++-- | 'YesodDispatchNested' is a more flexible class than 'YesodDispatch',+-- and this instance is the proof. Given a @'Route' site@ we can dispatch+-- on it as normal, so this code path works. This allows us to create+-- generalized helpers and reuse them, ensuring that nested and top-level+-- dispatch remain equivalent. This instance therefore always returns+-- @'Just'@ — committing to 'yesodDispatch', which itself terminates in+-- a 404 on a miss — rather than the 'Nothing' that the class haddock+-- describes for /fragment/ instances. The only intended caller is the+-- terminal authority 'toWaiAppYreNested', where that 404 is exactly the+-- right answer; do not rely on this instance to signal fallthrough.+instance YesodDispatch site => YesodDispatchNested (Route site) where+ yesodDispatchNested _ _ _ yre req = Just $ yesodDispatch yre req++-- | Build a WAI 'W.Application' for @site@, indexed by a @url@ type that can+-- be rendered to a route of @site@ (via 'RedirectUrl').+--+-- Note that dispatch is purely path-based: the hand-written instances below+-- discard the @url@ value (@urlToDispatch _ = yesodDispatch@) and route on the+-- request's @pathInfo@. The @url@ argument exists only to select the instance+-- and to carry the 'RedirectUrl' constraint — it is not consulted at runtime.+--+-- This is equivalent to 'YesodDispatch' but allows us to vary on the+-- @url@ type, which lets us demand additional constraints in test+-- contexts.+--+-- @since 1.7.0.0+class RedirectUrl site url => UrlToDispatch url site where+ -- | @since 1.7.0.0+ urlToDispatch :: url -> YesodRunnerEnv site -> W.Application++instance YesodDispatch site => UrlToDispatch (Route site) site where+ urlToDispatch _ = yesodDispatch++instance (YesodDispatch site, RedirectUrl site (Route site, a)) => UrlToDispatch (Route site, a) site where+ urlToDispatch _ = yesodDispatch++instance YesodDispatch site => UrlToDispatch Text site where+ urlToDispatch _ = yesodDispatch++instance YesodDispatch site => UrlToDispatch String site where+ urlToDispatch _ = yesodDispatch++-- | This instance is the pattern upon which 'UrlToDispatch' instances are+-- generated for subsite fragments. Given the constraints, we are able to+-- call 'toWaiAppYreNested' at the @route@ type with the correct arguments.+-- This allows us to create an application for a fragment of a site.+--+-- @since 1.7.0.0+instance+ ( ParentSite route ~ site+ , YesodDispatchNested route+ , RedirectUrl site (WithParentArgs route)+ , Yesod site+ , ToParentRoute route+ )+ =>+ UrlToDispatch (WithParentArgs route) site+ where+ urlToDispatch (WithParentArgs args _) =+ toWaiAppYreNested (Proxy :: Proxy route) args+ class YesodSubDispatch sub master where yesodSubDispatch :: YesodSubRunnerEnv sub master -> W.Application +-- | Like 'YesodDispatchNested', but for subsites. This class enables+-- class-based delegation for nested routes within subsites, allowing+-- module separation and symmetry with 'YesodDispatchNested'.+--+-- When a 'YesodSubDispatchNested' instance exists for a nested route type,+-- 'mkYesodSubDispatch' will delegate to it instead of inlining the dispatch.+-- Use 'mkYesodSubDispatchInstance' to generate both 'YesodSubDispatch' and+-- 'YesodSubDispatchNested' instances in one splice.+--+-- @since 1.7.0.0+class RenderRouteNested a => YesodSubDispatchNested a where+ -- | Dispatches a request for a nested route fragment within a subsite.+ --+ -- Similar to 'yesodDispatchNested', but works with 'YesodSubRunnerEnv'+ -- instead of 'YesodRunnerEnv', making it suitable for subsites.+ -- The @master@ type is universally quantified since it's not determined+ -- by the nested route type.+ --+ -- @since 1.7.0.0+ yesodSubDispatchNested+ :: Proxy a+ -- ^ Type proxy to resolve ambiguity from non-injective type families+ -> ParentArgs a+ -- ^ The dynamic arguments from the parent route+ -> (a -> Route (ParentSite a))+ -- ^ Function to wrap the nested route in the parent constructor+ -> YesodSubRunnerEnv (ParentSite a) master+ -- ^ The subsite runner environment+ -> W.Request+ -- ^ The full WAI request+ -> Maybe ((W.Response -> IO W.ResponseReceived) -> IO W.ResponseReceived)+ -- ^ Returns 'Nothing' for fallthrough, or 'Just' a continuation+ instance YesodSubDispatch WaiSubsite master where yesodSubDispatch YesodSubRunnerEnv {..} = app where@@ -50,3 +196,50 @@ , rheRouteToMaster = ysreToParentRoute } in f hd { handlerEnv = rhe' }++-- | Build a WAI 'W.Application' that dispatches starting at a nested route+-- fragment @a@ (rather than the whole @'Route' site@), given a+-- 'YesodRunnerEnv' for the parent site and the parent route's 'ParentArgs'.+-- Handles @cleanPath@ redirects and a terminal 404 just like 'toWaiAppYre'.+--+-- @since 1.7.0.0+toWaiAppYreNested+ :: (Yesod (ParentSite a), YesodDispatchNested a, ToParentRoute a)+ => Proxy a+ -> ParentArgs a+ -> YesodRunnerEnv (ParentSite a)+ -> W.Application+toWaiAppYreNested proxy parentArgs yre req =+ case cleanPath site $ W.pathInfo req of+ Left pieces -> sendRedirect site pieces req+ Right pieces -> do+ let mapplication =+ yesodDispatchNested proxy parentArgs (toParentRoute parentArgs) yre req+ { W.pathInfo = pieces+ }+ case mapplication of+ Nothing ->+ yesodRunner (notFound :: HandlerFor site ()) yre Nothing req+ Just k ->+ k+ where+ site = yreSite yre+ sendRedirect :: Yesod master => master -> [Text] -> W.Application+ sendRedirect y segments' env sendResponse =+ sendResponse $ W.responseLBS status+ [ ("Content-Type", "text/plain")+ , ("Location", BL.toStrict $ toLazyByteString dest')+ ] "Redirecting"+ where+ -- Ensure that non-GET requests get redirected correctly. See:+ -- https://github.com/yesodweb/yesod/issues/951+ status+ | W.requestMethod env == "GET" = status301+ | otherwise = status307++ dest = joinPath y (resolveApproot y env) segments' []+ dest' =+ if S.null (W.rawQueryString env)+ then dest+ else dest `mappend`+ byteString (W.rawQueryString env)
+ src/Yesod/Core/Class/Dispatch/ToParentRoute.hs view
@@ -0,0 +1,18 @@+{-# language FlexibleContexts #-}++module Yesod.Core.Class.Dispatch.ToParentRoute where++import Yesod.Routes.Class++-- | Reconstruct a full parent route from a nested route fragment and its+-- 'ParentArgs' (the dynamic pieces consumed by the ancestor parents). This is+-- what lets a fragment dispatched in its own module be rendered back into the+-- master site's @'Route' ('ParentSite' a)@.+--+-- @since 1.7.0.0+class (RenderRoute (ParentSite a)) => ToParentRoute a where+ -- | @since 1.7.0.0+ toParentRoute :: ParentArgs a -> a -> Route (ParentSite a)++instance (RenderRoute a) => ToParentRoute (Route a) where+ toParentRoute _ = id
src/Yesod/Core/Class/Handler.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE CPP #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-}+ module Yesod.Core.Class.Handler ( MonadHandler (..) , MonadWidget (..)@@ -19,7 +20,9 @@ import Data.Conduit.Internal (Pipe, ConduitM) import Control.Monad.Trans.Identity ( IdentityT)+#if !MIN_VERSION_transformers(0,6,0) import Control.Monad.Trans.List ( ListT )+#endif import Control.Monad.Trans.Maybe ( MaybeT ) import Control.Monad.Trans.Except ( ExceptT ) import Control.Monad.Trans.Reader ( ReaderT )@@ -76,7 +79,9 @@ #define GO(T) instance MonadHandler m => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; type SubHandlerSite (T m) = SubHandlerSite m; liftHandler = lift . liftHandler; liftSubHandler = lift . liftSubHandler #define GOX(X, T) instance (X, MonadHandler m) => MonadHandler (T m) where type HandlerSite (T m) = HandlerSite m; type SubHandlerSite (T m) = SubHandlerSite m; liftHandler = lift . liftHandler; liftSubHandler = lift . liftSubHandler GO(IdentityT)+#if !MIN_VERSION_transformers(0,6,0) GO(ListT)+#endif GO(MaybeT) GO(ExceptT e) GO(ReaderT r)@@ -104,7 +109,9 @@ #define GO(T) instance MonadWidget m => MonadWidget (T m) where liftWidget = lift . liftWidget #define GOX(X, T) instance (X, MonadWidget m) => MonadWidget (T m) where liftWidget = lift . liftWidget GO(IdentityT)+#if !MIN_VERSION_transformers(0,6,0) GO(ListT)+#endif GO(MaybeT) GO(ExceptT e) GO(ReaderT r)
src/Yesod/Core/Class/Yesod.hs view
@@ -1,7 +1,8 @@-{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+ module Yesod.Core.Class.Yesod where import Yesod.Core.Content@@ -24,7 +25,7 @@ import Data.List (foldl', nub) import qualified Data.Map as Map import Data.Maybe (catMaybes)-import Data.Monoid+import Data.Monoid (Last (..)) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE@@ -54,6 +55,7 @@ import Data.CaseInsensitive (CI) import qualified Network.Wai.Request import Data.IORef+import UnliftIO (SomeException, catch, MonadUnliftIO) -- | Define settings for a Yesod applications. All methods have intelligent -- defaults, and therefore no implementation is required.@@ -70,6 +72,16 @@ approot :: Approot site approot = guessApproot + -- | @since 1.6.24.0+ -- allows the user to specify how exceptions are cought.+ -- by default all async exceptions are thrown and synchronous+ -- exceptions render a 500 page.+ -- To catch all exceptions (even async) to render a 500 page,+ -- set this to 'UnliftIO.Exception.catchSyncOrAsync'. Beware+ -- this may have negative effects with functions like 'timeout'.+ catchHandlerExceptions :: MonadUnliftIO m => site -> m a -> (SomeException -> m a) -> m a+ catchHandlerExceptions _ = catch+ -- | Output error response pages. -- -- Default value: 'defaultErrorHandler'.@@ -87,6 +99,8 @@ <html> <head> <title>#{pageTitle p}+ $maybe description <- pageDescription p+ <meta name="description" content="#{description}"> ^{pageHead p} <body> $forall (status, msg) <- msgs@@ -249,6 +263,16 @@ jsAttributes :: site -> [(Text, Text)] jsAttributes _ = [] + -- | Same as @jsAttributes@ but allows you to run arbitrary Handler code+ --+ -- This is useful if you need to add a randomised nonce value to the script+ -- tag generated by @widgetFile@. If this function is overridden then+ -- @jsAttributes@ is ignored.+ --+ -- @since 1.6.16+ jsAttributesHandler :: HandlerFor site [(Text, Text)]+ jsAttributesHandler = jsAttributes <$> getYesod+ -- | Create a session backend. Returning 'Nothing' disables -- sessions. If you'd like to change the way that the session -- cookies are created, take a look at@@ -520,15 +544,18 @@ widgetToPageContent :: Yesod site => WidgetFor site () -> HandlerFor site (PageContent (Route site))-widgetToPageContent w = HandlerFor $ \hd -> do+widgetToPageContent w = do+ jsAttrs <- jsAttributesHandler+ HandlerFor $ \hd -> do master <- unHandlerFor getYesod hd ref <- newIORef mempty unWidgetFor w WidgetData { wdRef = ref , wdHandler = hd }- GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head') <- readIORef ref+ GWData (Body body) (Last mTitle) (Last mDescription) scripts' stylesheets' style jscript (Head head') <- readIORef ref let title = maybe mempty unTitle mTitle+ description = unDescription <$> mDescription scripts = runUniqueList scripts' stylesheets = runUniqueList stylesheets' @@ -564,9 +591,9 @@ ^{mkScriptTag s} $maybe j <- jscript $maybe s <- jsLoc- <script src="#{s}" *{jsAttributes master}>+ <script src="#{s}" *{jsAttrs}> $nothing- <script>^{jelper j}+ <script *{jsAttrs}>^{jelper j} |] headAll = [hamlet|@@ -598,7 +625,7 @@ ^{regularScriptLoad} |] - return $ PageContent title headAll $+ return $ PageContent title description headAll $ case jsLoader master of BottomOfBody -> bodyScript _ -> body@@ -627,7 +654,7 @@ let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r defaultMessageWidget "Not Found" [hamlet|<p>#{path'}|] provideRep $ return $ object ["message" .= ("Not Found" :: Text)]- provideRep $ return $ ("Not Found" :: Text)+ provideRep $ return ("Not Found" :: Text) -- For API requests. -- For a user with a browser,@@ -651,7 +678,7 @@ let apair u = ["authentication_url" .= rend u] content = maybe [] apair (authRoute site) return $ object $ ("message" .= ("Not logged in"::Text)):content- provideRep $ return $ ("Not logged in" :: Text)+ provideRep $ return ("Not logged in" :: Text) defaultErrorHandler (PermissionDenied msg) = selectRep $ do provideRep $ defaultLayout $ defaultMessageWidget@@ -670,7 +697,7 @@ <li>#{msg} |] provideRep $ return $ object ["message" .= ("Invalid Arguments" :: Text), "errors" .= ia]- provideRep $ return $ ("Invalid Arguments: " <> T.intercalate " " ia)+ provideRep $ return ("Invalid Arguments: " <> T.intercalate " " ia) defaultErrorHandler (InternalError e) = do $logErrorS "yesod-core" e
src/Yesod/Core/Content.hs view
@@ -1,9 +1,10 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE Rank2Types #-}-{-# LANGUAGE OverloadedStrings #-}+ module Yesod.Core.Content ( -- * Content Content (..)@@ -31,6 +32,7 @@ -- * Utilities , simpleContentType , contentTypeTypes+ , typedContentToSnippet -- * Evaluation strategy , DontFullyEvaluate (..) -- * Representations@@ -50,10 +52,12 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Builder as BB import Data.Text.Lazy (Text, pack) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8Builder) import qualified Data.Text.Lazy as TL+ import Data.ByteString.Builder (Builder, byteString, lazyByteString, stringUtf8) import Text.Hamlet (Html) import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)@@ -64,11 +68,16 @@ import qualified Data.Aeson as J import Data.Text.Lazy.Builder (toLazyText)+import Data.Void (Void, absurd) import Yesod.Core.Types import Text.Lucius (Css, renderCss) import Text.Julius (Javascript, unJavascript)+import qualified Network.Wai.Parse as NWP+import qualified Data.Int as I import Data.Word8 (_semicolon, _slash) import Control.Arrow (second)+import Control.Exception (Exception)+import Data.Maybe -- | Zero-length enumerator. emptyContent :: Content@@ -103,6 +112,8 @@ toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing instance ToContent () where toContent () = toContent B.empty+instance ToContent Void where+ toContent = absurd instance ToContent (ContentType, Content) where toContent = snd instance ToContent TypedContent where@@ -276,6 +287,8 @@ toTypedContent = id instance ToTypedContent () where toTypedContent () = TypedContent typePlain (toContent ())+instance ToTypedContent Void where+ toTypedContent = absurd instance ToTypedContent (ContentType, Content) where toTypedContent (ct, content) = TypedContent ct content instance ToTypedContent RepJson where
src/Yesod/Core/Dispatch.hs view
@@ -1,8 +1,12 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module Yesod.Core.Dispatch ( -- * Quasi-quoted routing parseRoutes@@ -10,13 +14,44 @@ , parseRoutesFile , parseRoutesFileNoCheck , mkYesod+ , mkYesodOpts , mkYesodWith -- ** More fine-grained , mkYesodData+ , mkYesodDataOpts , mkYesodSubData+ , mkYesodSubDataOpts , mkYesodDispatch+ , mkYesodDispatchOpts , mkYesodSubDispatch+ , mkYesodSubDispatchInstance+ , mkYesodSubDispatchInstanceOpts+ -- *** Splitting a subsite's nested routes across modules+ --+ -- | These re-exports let a separately-compiled module generate a+ -- subsite's @YesodSubDispatchNested@ instance by hand (see the+ -- subsite-route-splitting recipe in @docs/split-route-compilation.md@).+ -- 'mkNestedSubDispatchInstance' takes the resources as+ -- @['ResourceTree' 'String']@ (the 'parseRoutes' quasi-quoter's output)+ -- and parses them internally, so no manual type-parsing is needed.++ -- | @since 1.7.0.0+ , mkNestedSubDispatchInstance+ -- | @since 1.7.0.0+ , TyArgs (..)+ -- *** Route generation options+ , RouteOpts+ , defaultOpts+ , setEqDerived+ , setShowDerived+ , setReadDerived+ , setCreateResources+ , setFocusOnNestedRoute+ , unsetFocusOnNestedRoute+ , setParameterizedSubroute+ , setNestedRouteFallthrough -- *** Helpers+ , defaultGen , getGetMaxExpires -- ** Path pieces , PathPiece (..)@@ -25,12 +60,18 @@ -- * Convert to WAI , toWaiApp , toWaiAppPlain+ , UrlToDispatch(..)+ , mkYesodRunnerEnv+ , toWaiAppPlainNested , toWaiAppYre+ , toWaiAppYreNested , warp , warpDebug , warpEnv , mkDefaultMiddlewares , defaultMiddlewaresNoLogging+ -- * Subsite nested dispatch+ , YesodSubDispatchNested (..) -- * WAI subsites , WaiSubsite (..) , WaiSubsiteWithAuth (..)@@ -39,6 +80,8 @@ import Prelude hiding (exp) import Yesod.Core.Internal.TH import Language.Haskell.TH.Syntax (qLocation)+import Data.Proxy+import Yesod.Routes.Class import Web.PathPieces @@ -46,27 +89,34 @@ import Data.ByteString.Lazy.Char8 () +import Data.Bits ((.|.), finiteBitSize, shiftL) import Data.Text (Text) import qualified Data.ByteString as S-import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as S8-import Data.ByteString.Builder (byteString, toLazyByteString)-import Network.HTTP.Types (status301, status307)+#if !MIN_VERSION_wai_extra(3,1,14)+import Data.Default (def)+#endif import Yesod.Routes.Parse+import Yesod.Routes.TH (TyArgs (..)) import Yesod.Core.Types import Yesod.Core.Class.Yesod import Yesod.Core.Class.Dispatch-import Yesod.Core.Internal.Run import Text.Read (readMaybe) import System.Environment (getEnvironment)-import qualified System.Random as Random+import System.Entropy (getEntropy) import Control.AutoUpdate (mkAutoUpdate, defaultUpdateSettings, updateAction, updateFreq) import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123)+import Yesod.Core.Class.Dispatch.ToParentRoute import Network.Wai.Middleware.Autohead import Network.Wai.Middleware.AcceptOverride import Network.Wai.Middleware.RequestLogger-import Network.Wai.Middleware.Gzip+import Network.Wai.Middleware.Gzip (+ gzip,+#if MIN_VERSION_wai_extra(3,1,14)+ defaultGzipSettings,+#endif+ ) import Network.Wai.Middleware.MethodOverride import qualified Network.Wai.Handler.Warp@@ -81,19 +131,67 @@ -- used middlewares, please use 'toWaiApp'. toWaiAppPlain :: YesodDispatch site => site -> IO W.Application toWaiAppPlain site = do+ yre <- mkYesodRunnerEnv site+ return $ toWaiAppYre yre++-- | Construct a 'YesodRunnerEnv' for a site, initializing its logger, session+-- backend, and other runtime state. This is the environment threaded through+-- dispatch; 'toWaiAppPlain' and friends build one for you, but it is exposed so+-- callers can construct and reuse a runner environment directly.+--+-- @since 1.7.0.0+mkYesodRunnerEnv :: Yesod site => site -> IO (YesodRunnerEnv site)+mkYesodRunnerEnv site = do logger <- makeLogger site sb <- makeSessionBackend site getMaxExpires <- getGetMaxExpires- return $ toWaiAppYre YesodRunnerEnv- { yreLogger = logger- , yreSite = site- , yreSessionBackend = sb- , yreGen = defaultGen- , yreGetMaxExpires = getMaxExpires- }+ pure YesodRunnerEnv+ { yreLogger = logger+ , yreSite = site+ , yreSessionBackend = sb+ , yreGen = defaultGen+ , yreGetMaxExpires = getMaxExpires+ } +-- | Convert the given argument into a WAI application, executable with any WAI+-- handler. This function will provide no middlewares; if you want commonly+-- used middlewares, please use 'toWaiApp'.+--+-- This function allows you to create an 'Application' that only responds+-- to a subset of the 'site' routes. This is really only useful for writing+-- tests, but it does allow you to write tests that only depend on a subset+-- of the handlers instead of all handlers.+--+-- @since 1.7.0.0+toWaiAppPlainNested+ :: ( site ~ ParentSite route+ , Yesod site+ , YesodDispatchNested route+ , ToParentRoute route+ )+ => Proxy route+ -> ParentArgs route+ -> site+ -> IO W.Application+toWaiAppPlainNested proxy parentArgs site = do+ yre <- mkYesodRunnerEnv site+ return $ toWaiAppYreNested proxy parentArgs yre++-- | Generate a random number uniformly distributed in the full range+-- of 'Int'.+--+-- Note: Before 1.6.20, this generates pseudo-random number in an+-- unspecified range. The range size may not be a power of 2. Since+-- 1.6.20, this uses a secure entropy source and generates in the full+-- range of 'Int'.+--+-- @since 1.6.21.0 defaultGen :: IO Int-defaultGen = Random.getStdRandom Random.next+defaultGen = bsToInt <$> getEntropy bytes+ where+ bits = finiteBitSize (undefined :: Int)+ bytes = div (bits + 7) 8+ bsToInt = S.foldl' (\v i -> shiftL v 8 .|. fromIntegral i) 0 -- | Pure low level function to construct WAI application. Usefull -- when you need not standard way to run your app, or want to embed it@@ -101,33 +199,7 @@ -- -- @since 1.4.29 toWaiAppYre :: YesodDispatch site => YesodRunnerEnv site -> W.Application-toWaiAppYre yre req =- case cleanPath site $ W.pathInfo req of- Left pieces -> sendRedirect site pieces req- Right pieces -> yesodDispatch yre req- { W.pathInfo = pieces- }- where- site = yreSite yre- sendRedirect :: Yesod master => master -> [Text] -> W.Application- sendRedirect y segments' env sendResponse =- sendResponse $ W.responseLBS status- [ ("Content-Type", "text/plain")- , ("Location", BL.toStrict $ toLazyByteString dest')- ] "Redirecting"- where- -- Ensure that non-GET requests get redirected correctly. See:- -- https://github.com/yesodweb/yesod/issues/951- status- | W.requestMethod env == "GET" = status301- | otherwise = status307-- dest = joinPath y (resolveApproot y env) segments' []- dest' =- if S.null (W.rawQueryString env)- then dest- else dest `mappend`- byteString (W.rawQueryString env)+toWaiAppYre = toWaiAppYreNested (Proxy :: Proxy (Route site)) () -- | Same as 'toWaiAppPlain', but provides a default set of middlewares. This -- set may change with future releases, but currently covers:@@ -172,6 +244,16 @@ -- middlewares. This set may change at any point without a breaking version -- number. Currently, it includes: --+-- * Logging+--+-- * GZIP compression+--+-- * Automatic HEAD method handling+--+-- * Request method override with the _method query string parameter+--+-- * Accept header override with the _accept query string parameter+-- -- If you need more fine-grained control of middlewares, please use 'toWaiApp' -- directly. --@@ -209,7 +291,12 @@ -- Since 1.2.0 mkDefaultMiddlewares :: Logger -> IO W.Middleware mkDefaultMiddlewares logger = do- logWare <- mkRequestLogger def+ logWare <- mkRequestLogger+#if MIN_VERSION_wai_extra(3,1,8)+ defaultRequestLoggerSettings+#else+ def+#endif { destination = Network.Wai.Middleware.RequestLogger.Logger $ loggerSet logger , outputFormat = Apache FromSocket }@@ -219,7 +306,14 @@ -- -- Since 1.2.12 defaultMiddlewaresNoLogging :: W.Middleware-defaultMiddlewaresNoLogging = acceptOverride . autohead . gzip def . methodOverride+defaultMiddlewaresNoLogging = acceptOverride . autohead . gzip gzipSettings . methodOverride+ where+ gzipSettings =+#if MIN_VERSION_wai_extra(3,1,14)+ defaultGzipSettings+#else+ def+#endif -- | Deprecated synonym for 'warp'. warpDebug :: YesodDispatch site => Int -> site -> IO ()
src/Yesod/Core/Handler.hs view
@@ -1,15 +1,19 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+ --------------------------------------------------------- -- -- Module : Yesod.Handler@@ -91,7 +95,8 @@ , permissionDeniedI , invalidArgs , invalidArgsI- -- ** Short-circuit responses.+ -- ** Short-circuit responses+ -- $rollbackWarning , sendFile , sendFilePart , sendResponse@@ -244,10 +249,10 @@ import qualified Data.IORef as I import Data.Maybe (listToMaybe, mapMaybe) import Data.Typeable (Typeable)+import Data.Kind (Type) import Web.PathPieces (PathPiece(..)) import Yesod.Core.Class.Handler import Yesod.Core.Types-import Yesod.Routes.Class (Route) import Data.ByteString.Builder (Builder) import Data.CaseInsensitive (CI, original) import qualified Data.Conduit.List as CL@@ -259,8 +264,10 @@ import qualified Data.Word8 as W8 import qualified Data.Foldable as Fold import Control.Monad.Logger (MonadLogger, logWarnS)+import Yesod.Routes.Class (WithParentArgs(..), ParentSite(..))+import Yesod.Core.Class.Dispatch.ToParentRoute (ToParentRoute(..)) -type HandlerT site (m :: * -> *) = HandlerFor site+type HandlerT site (m :: Type -> Type) = HandlerFor site {-# DEPRECATED HandlerT "Use HandlerFor directly" #-} get :: MonadHandler m => m GHState@@ -369,10 +376,10 @@ getCurrentRoute :: MonadHandler m => m (Maybe (Route (HandlerSite m))) getCurrentRoute = rheRoute <$> askHandlerEnv --- | Returns a function that runs 'HandlerT' actions inside @IO@.+-- | Returns a function that runs 'HandlerFor' actions inside @IO@. ----- Sometimes you want to run an inner 'HandlerT' action outside--- the control flow of an HTTP request (on the outer 'HandlerT'+-- Sometimes you want to run an inner 'HandlerFor' action outside+-- the control flow of an HTTP request (on the outer 'HandlerFor' -- action). For example, you may want to spawn a new thread: -- -- @@@ -380,30 +387,30 @@ -- getFooR = do -- runInnerHandler <- handlerToIO -- liftIO $ forkIO $ runInnerHandler $ do--- /Code here runs inside GHandler but on a new thread./--- /This is the inner GHandler./+-- /Code here runs inside HandlerFor but on a new thread./+-- /This is the inner HandlerFor./ -- ... -- /Code here runs inside the request's control flow./--- /This is the outer GHandler./+-- /This is the outer HandlerFor./ -- ... -- @ -- -- Another use case for this function is creating a stream of--- server-sent events using 'GHandler' actions (see+-- server-sent events using 'HandlerFor' actions (see -- @yesod-eventsource@). ----- Most of the environment from the outer 'GHandler' is preserved--- on the inner 'GHandler', however:+-- Most of the environment from the outer 'HandlerFor' is preserved+-- on the inner 'HandlerFor', however: -- -- * The request body is cleared (otherwise it would be very -- difficult to prevent huge memory leaks). ----- * The cache is cleared (see 'CacheKey').+-- * The cache is cleared (see 'cached'). ----- Changes to the response made inside the inner 'GHandler' are+-- Changes to the response made inside the inner 'HandlerFor' are -- ignored (e.g., session variables, cookies, response headers).--- This allows the inner 'GHandler' to outlive the outer--- 'GHandler' (e.g., on the @forkIO@ example above, a response+-- This allows the inner 'HandlerFor' to outlive the outer+-- 'HandlerFor' (e.g., on the @forkIO@ example above, a response -- may be sent to the client without killing the new thread). handlerToIO :: MonadIO m => HandlerFor site (HandlerFor site a -> m a) handlerToIO =@@ -413,9 +420,15 @@ where oldReq = handlerRequest oldHandlerData oldWaiReq = reqWaiRequest oldReq+#if MIN_VERSION_wai(3,2,4)+ newWaiReq =+ W.setRequestBodyChunks (return mempty) $+ oldWaiReq { W.requestBodyLength = W.KnownLength 0 }+#else newWaiReq = oldWaiReq { W.requestBody = return mempty , W.requestBodyLength = W.KnownLength 0 }+#endif oldEnv = handlerEnv oldHandlerData newState <- liftIO $ do oldState <- I.readIORef (handlerState oldHandlerData)@@ -428,7 +441,7 @@ -- xx From this point onwards, no references to oldHandlerData xx liftIO $ evaluate (newReq `seq` oldEnv `seq` newState `seq` ()) - -- Return GHandler running function.+ -- Return HandlerFor running function. return $ \(HandlerFor f) -> liftIO $ runResourceT $ withInternalState $ \resState -> do@@ -607,6 +620,20 @@ getMessage :: MonadHandler m => m (Maybe Html) getMessage = fmap (fmap snd . listToMaybe) getMessages +-- $rollbackWarning+--+-- Note that since short-circuiting is implemented by using exceptions,+-- using e.g. 'sendStatusJSON' inside a runDB block+-- will result in the database actions getting rolled back:+--+-- @+-- runDB $ do+-- userId <- insert $ User "username" "email@example.com"+-- postId <- insert $ BlogPost "title" "hi there!"+-- /The previous two inserts will be rolled back./+-- sendStatusJSON Status.status200 ()+-- @+ -- | Bypass remaining handler code and output the given file. -- -- For some backends, this is more efficient than reading in the file to@@ -1008,6 +1035,17 @@ r <- getUrlRender return $ r url +instance+ ( RedirectUrl master (Route master)+ , ParentSite url ~ master+ , ToParentRoute url+ )+ =>+ RedirectUrl master (WithParentArgs url)+ where+ toTextUrl (WithParentArgs args url) =+ toTextUrl (toParentRoute args url)+ instance (key ~ Text, val ~ Text) => RedirectUrl master (Route master, [(key, val)]) where toTextUrl (url, params) = do r <- getUrlRenderParams@@ -1022,7 +1060,7 @@ -- > redirect (NewsfeedR :#: storyId) -- -- @since 1.2.9.-data Fragment a b = a :#: b deriving (Show, Typeable)+data Fragment a b = a :#: b deriving Show instance (RedirectUrl master a, PathPiece b) => RedirectUrl master (Fragment a b) where toTextUrl (a :#: b) = (\ua -> T.concat [ua, "#", toPathPiece b]) <$> toTextUrl a@@ -1069,13 +1107,15 @@ <html> <head> <title>Redirecting...- <body onload="document.getElementById('form').submit()">+ <body> <form id="form" method="post" action=#{urlText}> $maybe token <- reqToken req <input type=hidden name=#{defaultCsrfParamName} value=#{token}> <noscript> <p>Javascript has been disabled; please click on the button below to be redirected. <input type="submit" value="Continue">+ <script>+ window.onload = function() { document.getElementById('form').submit(); }; |] >>= sendResponse -- | Wraps the 'Content' generated by 'hamletToContent' in a 'RepHtml'.@@ -1209,10 +1249,10 @@ -- Languages are determined based on the following (in descending order -- of preference): ----- * The _LANG user session variable.--- -- * The _LANG get parameter. --+-- * The _LANG user session variable.+-- -- * The _LANG cookie. -- -- * Accept-Language HTTP header.@@ -1221,11 +1261,12 @@ -- If a matching language is not found the default language will be used. -- -- This is handled by parseWaiRequest (not exposed).+--+-- __NOTE__: Before version @1.6.19.0@, this function prioritized the session+-- variable above all other sources.+-- languages :: MonadHandler m => m [Text]-languages = do- mlang <- lookupSession langKey- langs <- reqLangs <$> getRequest- return $ maybe id (:) mlang langs+languages = reqLangs <$> getRequest lookup' :: Eq a => a -> [(a, b)] -> [b] lookup' a = map snd . filter (\x -> a == fst x)@@ -1415,7 +1456,11 @@ rawRequestBody = do req <- lift waiRequest let loop = do+#if MIN_VERSION_wai(3,2,2)+ bs <- liftIO $ W.getRequestBodyChunk req+#else bs <- liftIO $ W.requestBody req+#endif unless (S.null bs) $ do yield bs loop@@ -1447,8 +1492,8 @@ -- | Use a @Source@ for the response body. ----- Note that, for ease of use, the underlying monad is a @HandlerT@. This--- implies that you can run any @HandlerT@ action. However, since a streaming+-- Note that, for ease of use, the underlying monad is a @HandlerFor@. This+-- implies that you can run any @HandlerFor@ action. However, since a streaming -- response occurs after the response headers have already been sent, some -- actions make no sense here. For example: short-circuit responses, setting -- headers, changing status codes, etc.@@ -1459,8 +1504,8 @@ -> HandlerFor site TypedContent respondSource ctype src = HandlerFor $ \hd -> -- Note that this implementation relies on the fact that the ResourceT- -- environment provided by the server is the same one used in HandlerT.- -- This is a safe assumption assuming the HandlerT is run correctly.+ -- environment provided by the server is the same one used in HandlerFor.+ -- This is a safe assumption assuming the HandlerFor is run correctly. return $ TypedContent ctype $ ContentSource $ transPipe (lift . flip unHandlerFor hd) src
src/Yesod/Core/Internal/LiteApp.hs view
@@ -1,9 +1,8 @@-{-# LANGUAGE TypeFamilies, PatternGuards, CPP #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE TypeFamilies #-}+ module Yesod.Core.Internal.LiteApp where -#if !(MIN_VERSION_base(4,11,0))-import Data.Semigroup (Semigroup(..))-#endif import Yesod.Routes.Class import Yesod.Core.Class.Yesod import Yesod.Core.Class.Dispatch@@ -47,9 +46,6 @@ instance Monoid LiteApp where mempty = LiteApp $ \_ _ -> Nothing-#if !(MIN_VERSION_base(4,11,0))- mappend = (<>)-#endif type LiteHandler = HandlerFor LiteApp type LiteWidget = WidgetFor LiteApp
src/Yesod/Core/Internal/Request.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE OverloadedStrings, CPP #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+ module Yesod.Core.Internal.Request ( parseWaiRequest , RequestBodyContents@@ -49,9 +51,12 @@ limitRequestBody :: Word64 -> W.Request -> IO W.Request limitRequestBody maxLen req = do ref <- newIORef maxLen- return req- { W.requestBody = do+ let bd = do+#if MIN_VERSION_wai(3,2,2)+ bs <- W.getRequestBodyChunk req+#else bs <- W.requestBody req+#endif remaining <- readIORef ref let len = fromIntegral $ S8.length bs remaining' = remaining - len@@ -60,13 +65,17 @@ else do writeIORef ref remaining' return bs- }+#if MIN_VERSION_wai(3,2,4)+ return $ W.setRequestBodyChunks bd req+#else+ return req { W.requestBody = bd }+#endif tooLargeResponse :: Word64 -> Word64 -> W.Response tooLargeResponse maxLen bodyLen = W.responseLBS (Status 413 "Too Large") [("Content-Type", "text/plain")]- (L.concat + (L.concat [ "Request body too large to be processed. The maximum size is " , (LS8.pack (show maxLen)) , " bytes; your request body was "@@ -129,7 +138,7 @@ -- Already have a token, use it. Just bs -> Left $ Just $ decodeUtf8With lenientDecode bs -- Don't have a token, get a random generator and make a new one.- Nothing -> Right $ fmap Just . randomString 10+ Nothing -> Right $ fmap Just . randomString 40 | otherwise = Left Nothing textQueryString :: W.Request -> [(Text, Text)]
src/Yesod/Core/Internal/Response.hs view
@@ -1,6 +1,5 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}+ module Yesod.Core.Internal.Response where import Data.ByteString (ByteString)
src/Yesod/Core/Internal/Run.hs view
@@ -1,14 +1,29 @@+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE PatternGuards #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE FlexibleContexts #-}-module Yesod.Core.Internal.Run where+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-} +module Yesod.Core.Internal.Run+ ( toErrorHandler+ , errFromShow+ , basicRunHandler+ , handleError+ , handleContents+ , evalFallback+ , runHandler+ , safeEh+ , runFakeHandler+ , yesodRunner+ , yesodRender+ , resolveApproot+ )+ where -import Yesod.Core.Internal.Response+import Yesod.Core.Internal.Response import Data.ByteString.Builder (toLazyByteString) import qualified Data.ByteString.Lazy as BL import Control.Monad.IO.Class (MonadIO, liftIO)@@ -28,7 +43,6 @@ import Language.Haskell.TH.Syntax (Loc, qLocation) import qualified Network.HTTP.Types as H import Network.Wai-import Network.Wai.Internal import System.Log.FastLogger (LogStr, toLogStr) import Yesod.Core.Content import Yesod.Core.Class.Yesod@@ -36,7 +50,7 @@ import Yesod.Core.Internal.Request (parseWaiRequest, tooLargeResponse) import Yesod.Core.Internal.Util (getCurrentMaxExpiresRFC1123)-import Yesod.Routes.Class (Route, renderRoute)+import Yesod.Routes.Class (RenderRoute (..)) import Control.DeepSeq (($!!), NFData) import UnliftIO.Exception @@ -71,7 +85,7 @@ -- Run the handler itself, capturing any runtime exceptions and -- converting them into a @HandlerContents@- contents' <- catchAny+ contents' <- rheCatchHandlerExceptions rhe (do res <- unHandlerFor handler (hd istate) tc <- evaluate (toTypedContent res)@@ -172,16 +186,19 @@ -- | Evaluate the given value. If an exception is thrown, use it to -- replace the provided contents and then return @mempty@ in place of the -- evaluated value.+--+-- Note that this also catches async exceptions. evalFallback :: (Monoid w, NFData w)- => HandlerContents+ => (forall a. IO a -> (SomeException -> IO a) -> IO a)+ -> HandlerContents -> w -> IO (w, HandlerContents)-evalFallback contents val = catchAny+evalFallback catcher contents val = catcher (fmap (, contents) (evaluate $!! val)) (fmap ((mempty, ) . HCError) . toErrorHandler) -- | Function used internally by Yesod in the process of converting a--- 'HandlerT' into an 'Application'. Should not be needed by users.+-- 'HandlerFor' into an 'Application'. Should not be needed by users. runHandler :: ToTypedContent c => RunHandlerEnv site site -> HandlerFor site c@@ -192,8 +209,8 @@ -- Evaluate the unfortunately-lazy session and headers, -- propagating exceptions into the contents- (finalSession, contents1) <- evalFallback contents0 (ghsSession state)- (headers, contents2) <- evalFallback contents1 (appEndo (ghsHeaders state) [])+ (finalSession, contents1) <- evalFallback rheCatchHandlerExceptions contents0 (ghsSession state)+ (headers, contents2) <- evalFallback rheCatchHandlerExceptions contents1 (appEndo (ghsHeaders state) []) contents3 <- (evaluate contents2) `catchAny` (fmap HCError . toErrorHandler) -- Convert the HandlerContents into the final YesodResponse@@ -216,27 +233,27 @@ (toContent ("Internal Server Error" :: S.ByteString)) (reqSession req) --- | Run a 'HandlerT' completely outside of Yesod. This+-- | Run a 'HandlerFor' completely outside of Yesod. This -- function comes with many caveats and you shouldn't use it -- unless you fully understand what it's doing and how it works. -- -- As of now, there's only one reason to use this function at--- all: in order to run unit tests of functions inside 'HandlerT'+-- all: in order to run unit tests of functions inside 'HandlerFor' -- but that aren't easily testable with a full HTTP request. -- Even so, it's better to use @wai-test@ or @yesod-test@ instead -- of using this function. -- -- This function will create a fake HTTP request (both @wai@'s -- 'Request' and @yesod@'s 'Request') and feed it to the--- @HandlerT@. The only useful information the @HandlerT@ may+-- @HandlerFor@. The only useful information the @HandlerFor@ may -- get from the request is the session map, which you must supply -- as argument to @runFakeHandler@. All other fields contain -- fake information, which means that they can be accessed but -- won't have any useful information. The response of the--- @HandlerT@ is completely ignored, including changes to the+-- @HandlerFor@ is completely ignored, including changes to the -- session, cookies or headers. We only return you the--- @HandlerT@'s return value.-runFakeHandler :: (Yesod site, MonadIO m) =>+-- @HandlerFor@'s return value.+runFakeHandler :: forall site m a . (Yesod site, MonadIO m) => SessionMap -> (site -> Logger) -> site@@ -257,6 +274,7 @@ , rheLog = messageLoggerSource site $ logger site , rheOnError = errHandler , rheMaxExpires = maxExpires+ , rheCatchHandlerExceptions = catchHandlerExceptions site } handler' errHandler err req = do@@ -267,23 +285,12 @@ typePlain (toContent ("runFakeHandler: errHandler" :: S8.ByteString)) (reqSession req)- fakeWaiRequest = Request+ fakeWaiRequest = defaultRequest { requestMethod = "POST" , httpVersion = H.http11 , rawPathInfo = "/runFakeHandler/pathInfo"- , rawQueryString = ""- , requestHeaderHost = Nothing- , requestHeaders = []- , isSecure = False , remoteHost = error "runFakeHandler-remoteHost" , pathInfo = ["runFakeHandler", "pathInfo"]- , queryString = []- , requestBody = return mempty- , vault = mempty- , requestBodyLength = KnownLength 0- , requestHeaderRange = Nothing- , requestHeaderReferer = Nothing- , requestHeaderUserAgent = Nothing } fakeRequest = YesodRequest@@ -298,7 +305,7 @@ _ <- runResourceT $ yapp fakeRequest I.readIORef ret -yesodRunner :: (ToTypedContent res, Yesod site)+yesodRunner :: forall res site . (ToTypedContent res, Yesod site) => HandlerFor site res -> YesodRunnerEnv site -> Maybe (Route site)@@ -333,6 +340,7 @@ , rheLog = log' , rheOnError = safeEh log' , rheMaxExpires = maxExpires+ , rheCatchHandlerExceptions = catchHandlerExceptions yreSite } rhe = rheSafe { rheOnError = runHandler rheSafe . errorHandler@@ -343,7 +351,6 @@ yar <- runInternalState (runHandler rhe handler yreq') is yarToResponse yar saveSession yreq' req is sendResponse where- mmaxLen = maximumContentLength yreSite route handler = yesodMiddleware handler' yesodRender :: Yesod y
src/Yesod/Core/Internal/TH.hs view
@@ -1,45 +1,98 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleContexts #-}-module Yesod.Core.Internal.TH where+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-} +module Yesod.Core.Internal.TH+ ( mkYesod+ , mkYesodOpts++ , mkYesodWith++ , mkYesodData+ , mkYesodDataOpts++ , mkYesodSubData+ , mkYesodSubDataOpts++ , mkYesodWithParser+ , mkYesodWithParserOpts++ , mkYesodDispatch+ , mkYesodDispatchOpts++ , masterTypeSyns++ , mkYesodGeneral+ , mkYesodGeneralOpts++ , mkMDS+ , mkDispatchInstance++ , mkYesodSubDispatch+ , mkYesodSubDispatchInstance+ , mkYesodSubDispatchInstanceOpts+ , mkNestedSubDispatchInstance++ , subTopDispatch++ , RouteOpts+ , defaultOpts+ , setEqDerived+ , setShowDerived+ , setReadDerived+ , setCreateResources+ , setFocusOnNestedRoute+ , unsetFocusOnNestedRoute+ , setParameterizedSubroute+ , setNestedRouteFallthrough+ )+ where+ import Prelude hiding (exp) import Yesod.Core.Handler- import Language.Haskell.TH hiding (cxt, instanceD) import Language.Haskell.TH.Syntax--import qualified Network.Wai as W- import Data.ByteString.Lazy.Char8 () import Data.List (foldl')-import Control.Monad (replicateM, void)-import Text.Parsec (parse, many1, many, eof, try, option, sepBy1)-import Text.ParserCombinators.Parsec.Char (alphaNum, spaces, string, char)-+import Data.Maybe+import Control.Monad import Yesod.Routes.TH+import Yesod.Routes.TH.Dispatch+ (parseYesodName, mkYesodSubDispatchWithDelegate, SameSpliceNestedInstances (..))+import Yesod.Routes.TH.Internal (instanceD, typeArity, assertNestedSubArity, ArityCallSite(..), SubsiteName(..), SubsiteArity(..), resolveRouteCon, nestedInstanceExists) import Yesod.Routes.Parse import Yesod.Core.Types-import Yesod.Core.Class.Dispatch-import Yesod.Core.Internal.Run+import Yesod.Core.Class.Dispatch (YesodSubDispatch(..), YesodSubDispatchNested(..)) -- | Generates URL datatype and site function for the given 'Resource's. This -- is used for creating sites, /not/ subsites. See 'mkYesodSubData' and 'mkYesodSubDispatch' for the latter. -- Use 'parseRoutes' to create the 'Resource's. ----- Contexts and type variables in the name of the datatype are parsed. +-- Contexts and type variables in the name of the datatype are parsed. -- For example, a datatype @App a@ with typeclass constraint @MyClass a@ can be written as @\"(MyClass a) => App a\"@. mkYesod :: String -- ^ name of the argument datatype -> [ResourceTree String] -> Q [Dec]-mkYesod name = fmap (uncurry (++)) . mkYesodWithParser name False return+mkYesod = mkYesodOpts defaultOpts +-- | `mkYesod` but with custom options.+--+-- @since 1.6.25.0+mkYesodOpts :: RouteOpts+ -> String+ -> [ResourceTree String]+ -> Q [Dec]+mkYesodOpts opts name = fmap (uncurry (++)) . mkYesodWithParserOpts opts name False return++ {-# DEPRECATED mkYesodWith "Contexts and type variables are now parsed from the name in `mkYesod`. <https://github.com/yesodweb/yesod/pull/1366>" #-}--- | Similar to 'mkYesod', except contexts and type variables are not parsed. --- Instead, they are explicitly provided. +-- | Similar to 'mkYesod', except contexts and type variables are not parsed.+-- Instead, they are explicitly provided. -- You can write @(MyClass a) => App a@ with @mkYesodWith [[\"MyClass\",\"a\"]] \"App\" [\"a\"] ...@. mkYesodWith :: [[String]] -- ^ list of contexts -> String -- ^ name of the argument datatype@@ -48,71 +101,95 @@ -> Q [Dec] mkYesodWith cxts name args = fmap (uncurry (++)) . mkYesodGeneral cxts name args False return + -- | Sometimes, you will want to declare your routes in one file and define -- your handlers elsewhere. For example, this is the only way to break up a -- monolithic file into smaller parts. Use this function, paired with -- 'mkYesodDispatch', to do just that. mkYesodData :: String -> [ResourceTree String] -> Q [Dec]-mkYesodData name resS = fst <$> mkYesodWithParser name False return resS+mkYesodData = mkYesodDataOpts defaultOpts +-- | `mkYesodData` but with custom options.+--+-- @since 1.6.25.0+mkYesodDataOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]+mkYesodDataOpts opts name resS = fst <$> mkYesodWithParserOpts opts name False return resS++ mkYesodSubData :: String -> [ResourceTree String] -> Q [Dec]-mkYesodSubData name resS = fst <$> mkYesodWithParser name True return resS+mkYesodSubData = mkYesodSubDataOpts defaultOpts +-- |+--+-- @since 1.6.25.0+mkYesodSubDataOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]+mkYesodSubDataOpts opts name resS = fst <$> mkYesodWithParserOpts opts name True return resS+++-- | Run 'parseYesodName' in 'Q', failing the splice with the parse error+-- instead of returning an 'Either'. Shared by the @mkYesod@ and+-- @mkYesodSubDispatch@ entry points.+parseYesodNameQ :: String -> Q (String, [String], [[String]])+parseYesodNameQ name =+ case parseYesodName name of+ Left err -> fail err+ Right a -> pure a+ -- | Parses contexts and type arguments out of name before generating TH. mkYesodWithParser :: String -- ^ foundation type -> Bool -- ^ is this a subsite -> (Exp -> Q Exp) -- ^ unwrap handler -> [ResourceTree String] -> Q([Dec],[Dec])-mkYesodWithParser name isSub f resS = do- let (name', rest, cxt) = case parse parseName "" name of- Left err -> error $ show err- Right a -> a- mkYesodGeneral cxt name' rest isSub f resS+mkYesodWithParser = mkYesodWithParserOpts defaultOpts - where- parseName = do- cxt <- option [] parseContext- name' <- parseWord- args <- many parseWord- spaces- eof- return ( name', args, cxt)+-- | Parses contexts and type arguments out of name before generating TH.+--+-- @since 1.6.25.0+mkYesodWithParserOpts :: RouteOpts -- ^ Additional route options+ -> String -- ^ foundation type+ -> Bool -- ^ is this a subsite+ -> (Exp -> Q Exp) -- ^ unwrap handler+ -> [ResourceTree String]+ -> Q([Dec],[Dec])+mkYesodWithParserOpts opts name isSub f resS = do+ (name', rest, cxt) <- parseYesodNameQ name - parseWord = do- spaces- many1 alphaNum+ mkYesodGeneralOpts opts cxt name' rest isSub f resS - parseContext = try $ do- cxts <- parseParen parseContexts- spaces- _ <- string "=>"- return cxts - parseParen p = do- spaces- _ <- char '('- r <- p- spaces- _ <- char ')'- return r-- parseContexts = - sepBy1 (many1 parseWord) (spaces >> char ',' >> return ())- -- | See 'mkYesodData'. mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]-mkYesodDispatch name = fmap snd . mkYesodWithParser name False return+mkYesodDispatch = mkYesodDispatchOpts defaultOpts +-- | See 'mkYesodDataOpts'+--+-- @since 1.6.25.0+mkYesodDispatchOpts :: RouteOpts -> String -> [ResourceTree String] -> Q [Dec]+mkYesodDispatchOpts opts name = fmap snd . mkYesodWithParserOpts opts name False return+++-- | Build an instance context ('Cxt') from the parsed @=>@ class-application+-- groups produced by 'parseYesodName'. Each group is a class name applied to+-- type arguments, e.g. @["MyClass","a"]@ becomes @MyClass a@. Shared by+-- 'mkYesodGeneralOpts' and 'mkYesodSubDispatchInstanceOpts'.+buildAppCxt :: [[String]] -> Q Cxt+buildAppCxt = traverse $ \ctxs ->+ case ctxs of+ c:rest ->+ pure $ foldl' (\acc v -> acc `AppT` fst (nameToType v)) (ConT $ mkName c) rest+ [] -> fail $ "mkYesod: empty type-class context in route definition: " ++ show ctxs+ -- | Get the Handler and Widget type synonyms for the given site. masterTypeSyns :: [Name] -> Type -> [Dec] -- FIXME remove from here, put into the scaffolding itself? masterTypeSyns vs site =- [ TySynD (mkName "Handler") (fmap PlainTV vs)+ [ TySynD (mkName "Handler") (fmap plainTV vs) $ ConT ''HandlerFor `AppT` site- , TySynD (mkName "Widget") (fmap PlainTV vs)+ , TySynD (mkName "Widget") (fmap plainTV vs) $ ConT ''WidgetFor `AppT` site `AppT` ConT ''() ] + mkYesodGeneral :: [[String]] -- ^ Appliction context. Used in RenderRoute, RouteAttrs, and ParseRoute instances. -> String -- ^ foundation type -> [String] -- ^ arguments for the type@@ -120,101 +197,294 @@ -> (Exp -> Q Exp) -- ^ unwrap handler -> [ResourceTree String] -> Q([Dec],[Dec])-mkYesodGeneral appCxt' namestr mtys isSub f resS = do- let appCxt = fmap (\(c:rest) -> - foldl' (\acc v -> acc `AppT` nameToType v) (ConT $ mkName c) rest- ) appCxt'+mkYesodGeneral = mkYesodGeneralOpts defaultOpts++-- | Convert the parsed-from-source 'String' route types into real 'Type's at a+-- splice site. A malformed type or an unclosed @#{…}@ bracket surfaces as an+-- attributed compile error (via 'fail') rather than a raw 'error' thrown lazily+-- when the resulting tree is forced. This is the single 'String'-to-'Type'+-- boundary: callers downstream only ever handle @['ResourceTree' 'Type']@.+parseResourceTypes :: [ResourceTree String] -> Q [ResourceTree Type]+parseResourceTypes = traverse (traverse (\s -> dropBracketM s >>= parseTypeM))++-- | Generate a @YesodSubDispatchNested@ instance for a nested route within a+-- subsite, for hand-written split-route modules (see the subsite-splitting+-- recipe in @docs/split-route-compilation.md@).+--+-- Takes the resources as @['ResourceTree' 'String']@ — exactly the form the+-- @parseRoutes@ quasi-quoter produces — and parses them to 'Type' internally+-- via 'parseResourceTypes', so the caller never touches the partial+-- 'parseType'\/'dropBracket'. A malformed type fails the splice with an+-- attributed error instead.+--+-- @since 1.7.0.0+mkNestedSubDispatchInstance+ :: RouteOpts+ -> String -- ^ target nested route name+ -> Cxt -- ^ instance context+ -> TyArgs -- ^ type arguments+ -> (Exp -> Q Exp) -- ^ unwrapper+ -> [ResourceTree String] -- ^ all resources (as parsed from source)+ -> Q [Dec]+mkNestedSubDispatchInstance routeOpts target cxt tyargs unwrapper resS = do+ res <- parseResourceTypes resS+ -- Guard the top target's arity here. 'mkNestedDispatchInstanceWith' only+ -- arity-checks nested *children*, so without this a hand-written recipe+ -- pairing a parameterized subsite with an unparameterized target datatype+ -- would apply the subsite's type args to a kind-'Type' head and surface a+ -- cryptic kind error from generated code rather than this actionable one.+ -- A no-op when the datatype is not in scope (unknowable arity) or when the+ -- arities match.+ rc <- resolveRouteCon target+ assertNestedSubArity SubsiteCall (SubsiteName target)+ (SubsiteArity (tyArgsArity tyargs)) rc+ mkNestedDispatchInstanceWith SubsiteNested Nothing+ routeOpts target cxt tyargs unwrapper res++-- | The resolved foundation type shared by 'mkYesodGeneralOpts' and+-- 'mkYesodSubDispatchInstanceOpts': the @boundNames@ from the explicitly-written+-- type args plus enough fresh @t@ variables to fill the reified arity, the fully+-- applied @site@ type, and the parsed resources. The callers differ only in how+-- they roll these into 'TyArgs' (and the synonym-head vars), so that part stays+-- with each caller.+data ResolvedFoundation = ResolvedFoundation+ { rfBoundNames :: [(Type, Name)] -- ^ explicitly-written type args+ , rfFillVars :: [Name] -- ^ fresh vars filling the reified arity+ , rfSite :: Type -- ^ the fully applied site type+ , rfResources :: [ResourceTree Type]+ }++resolveFoundation :: String -> [String] -> [ResourceTree String] -> Q ResolvedFoundation+resolveFoundation namestr mtys resS = do mname <- lookupTypeName namestr- arity <- case mname of- Just name -> do- info <- reify name- return $- case info of- TyConI dec ->- case dec of- DataD _ _ vs _ _ _ -> length vs- NewtypeD _ _ vs _ _ _ -> length vs- TySynD _ vs _ -> length vs- _ -> 0- _ -> 0- _ -> return 0+ arity <- maybe (pure 0) typeArity mname let name = mkName namestr -- Generate as many variable names as the arity indicates vns <- replicateM (arity - length mtys) $ newName "t"- -- Base type (site type with variables)- let argtypes = fmap nameToType mtys ++ fmap VarT vns+ let boundNames = fmap nameToType mtys+ argtypes = fmap fst boundNames ++ fmap VarT vns site = foldl' AppT (ConT name) argtypes- res = map (fmap (parseType . dropBracket)) resS- renderRouteDec <- mkRenderRouteInstance appCxt site res- routeAttrsDec <- mkRouteAttrsInstance appCxt site res- dispatchDec <- mkDispatchInstance site appCxt f res- parseRoute <- mkParseRouteInstance appCxt site res+ res <- parseResourceTypes resS+ pure ResolvedFoundation+ { rfBoundNames = boundNames+ , rfFillVars = vns+ , rfSite = site+ , rfResources = res+ }++-- |+--+-- @since 1.6.25.0+mkYesodGeneralOpts :: RouteOpts -- ^ Options to adjust route creation+ -> [[String]] -- ^ Application context. Used in RenderRoute, RouteAttrs, and ParseRoute instances.+ -> String -- ^ foundation type+ -> [String] -- ^ arguments for the type+ -> Bool -- ^ is this a subsite+ -> (Exp -> Q Exp) -- ^ unwrap handler+ -> [ResourceTree String]+ -> Q([Dec],[Dec])+mkYesodGeneralOpts opts appCxt' namestr mtys isSub f resS = do+ appCxt <- buildAppCxt appCxt'+ foundation <- resolveFoundation namestr mtys resS+ -- The explicitly-written args plus the fresh vars filling the reified+ -- arity. Both are part of the site's full application ('rfSite'), so both+ -- must travel in the 'TyArgs' handed to 'discoveryMode' and the generators+ -- — otherwise a parameterized foundation invoked without explicit type args+ -- would be misclassified as monomorphic and emit ill-scoped nested+ -- instances.+ let fillNames = fmap (\v -> (VarT v, v)) (rfFillVars foundation)+ tyArgs = toTyArgs (rfBoundNames foundation ++ fillNames)+ -- typevars that should appear in synonym head+ let argvars = (fmap mkName . filter isTvar) mtys ++ rfFillVars foundation+ renderRouteDec <-+ mkRenderRouteInstanceOpts opts appCxt tyArgs (rfSite foundation) (rfResources foundation)+ routeAttrsDec <-+ case roFocusOnNestedRoute opts of+ Nothing -> do+ -- The flat 'RouteAttrs (Route site)' instance, plus — in+ -- nested-discovery mode — a 'RouteAttrsNested' instance for each+ -- child fragment, mirroring the RenderRouteNested /+ -- ParseRouteNested / YesodDispatchNested instances generated for+ -- the same children. Without this, 'routeAttrsNested ChildR'+ -- would fail to resolve for a single-module 'mkYesod' site even+ -- though the other nested-delegation methods resolve.+ flatAttrs <- mkRouteAttrsInstance appCxt (rfSite foundation) (rfResources foundation)+ nestedAttrs <-+ case discoveryMode opts tyArgs of+ NestedDiscovery ->+ mkRouteAttrsNestedInstances appCxt tyArgs (rfResources foundation)+ InlineCompat -> pure []+ pure (flatAttrs : nestedAttrs)+ Just target ->+ -- Apply the site's type arguments to the focused child's+ -- constructor, matching the focused ParseRoute / RenderRoute+ -- paths; a bare 'ConT' here is a kind error for a+ -- parameterized site.+ mkRouteAttrsInstanceFor+ appCxt+ (applyTyArgs (ConT (mkName target)) tyArgs)+ target+ (rfResources foundation)++ dispatchDec <-+ mkDispatchInstance opts (rfSite foundation) appCxt tyArgs f (rfResources foundation)+ parseRouteDec <-+ mkParseRouteInstanceOpts opts tyArgs appCxt (rfSite foundation) (rfResources foundation) let rname = mkName $ "resources" ++ namestr- eres <- lift resS- let resourcesDec =- [ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)- , FunD rname [Clause [] (NormalB eres) []]- ]+ resourcesDec <-+ if shouldCreateResources opts+ then do+ eres <- lift resS+ pure+ [ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)+ , FunD rname [Clause [] (NormalB eres) []]+ ]+ else do+ pure [] let dataDec = concat- [ [parseRoute]+ [ parseRouteDec , renderRouteDec- , [routeAttrsDec]- , resourcesDec- , if isSub then [] else masterTypeSyns vns site+ , routeAttrsDec+ , if isJust (roFocusOnNestedRoute opts) then [] else resourcesDec+ , if isSub || isJust (roFocusOnNestedRoute opts)+ then []+ else masterTypeSyns argvars (rfSite foundation) ] return (dataDec, dispatchDec) -mkMDS :: (Exp -> Q Exp) -> Q Exp -> MkDispatchSettings a site b-mkMDS f rh = MkDispatchSettings- { mdsRunHandler = rh- , mdsSubDispatcher =- [|\parentRunner getSub toParent env -> yesodSubDispatch- YesodSubRunnerEnv- { ysreParentRunner = parentRunner- , ysreGetSub = getSub- , ysreToParentRoute = toParent- , ysreParentEnv = env- }- |]- , mdsGetPathInfo = [|W.pathInfo|]- , mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]- , mdsMethod = [|W.requestMethod|]- , mds404 = [|void notFound|]- , mds405 = [|void badMethod|]- , mdsGetHandler = defaultGetHandler- , mdsUnwrapper = f- }+-- | Generate both 'YesodSubDispatch' and 'YesodSubDispatchNested' instances+-- for a parameterized subsite. This is the subsite equivalent of using+-- @mkYesod@ for top-level sites.+--+-- Usage:+--+-- @+-- mkYesodSubDispatchInstance "(MyClass a) => MySub a" resourcesMySub+-- @+--+-- This generates:+--+-- 1. A 'YesodSubDispatch' instance using 'mkYesodSubDispatch'+-- 2. 'YesodSubDispatchNested' instances for any nested route fragments+--+-- The generated instances quantify over the @master@ site and constrain it+-- only through the subsite's own class. For a parameterized subsite this+-- normally means the subsite's class carries a @subsite -> master@ functional+-- dependency (so @master@ is determined) and the using module enables+-- @UndecidableInstances@ (the instance contexts mention type-family+-- applications and non-variable arguments), along with @FlexibleContexts@,+-- @FlexibleInstances@, @MultiParamTypeClasses@ and @TypeFamilies@. The nested+-- route datatype must also declare exactly as many type parameters as the+-- subsite has type arguments (see the arity note below).+--+-- @since 1.7.0.0+mkYesodSubDispatchInstance+ :: String -- ^ Foundation type with optional context, e.g. @"(MyClass a) => MySub a"@+ -> [ResourceTree String] -- ^ Resources (e.g. @resourcesMySub@)+ -> Q [Dec]+mkYesodSubDispatchInstance = mkYesodSubDispatchInstanceOpts defaultOpts --- | If the generation of @'YesodDispatch'@ instance require finer--- control of the types, contexts etc. using this combinator. You will--- hardly need this generality. However, in certain situations, like--- when writing library/plugin for yesod, this combinator becomes--- handy.-mkDispatchInstance :: Type -- ^ The master site type- -> Cxt -- ^ Context of the instance- -> (Exp -> Q Exp) -- ^ Unwrap handler- -> [ResourceTree c] -- ^ The resource- -> DecsQ-mkDispatchInstance master cxt f res = do- clause' <- mkDispatchClause (mkMDS f [|yesodRunner|]) res- let thisDispatch = FunD 'yesodDispatch [clause']- return [instanceD cxt yDispatch [thisDispatch]]- where- yDispatch = ConT ''YesodDispatch `AppT` master+-- | Like 'mkYesodSubDispatchInstance', but takes a 'RouteOpts' so a subsite can+-- control nested-route generation — most usefully 'setNestedRouteFallthrough'+-- (which 'mkYesodSubDispatchInstance' leaves at its default of 'False'). The+-- flag is threaded into both the @yesodSubDispatch@ body (so the subsite's own+-- top-level parent clauses fall through to later siblings on an inner miss) and+-- the generated @YesodSubDispatchNested@ fragment instances.+--+-- @since 1.7.0.0+mkYesodSubDispatchInstanceOpts+ :: RouteOpts+ -> String -- ^ Foundation type with optional context, e.g. @"(MyClass a) => MySub a"@+ -> [ResourceTree String] -- ^ Resources (e.g. @resourcesMySub@)+ -> Q [Dec]+mkYesodSubDispatchInstanceOpts opts nameStr resS = do+ -- Parse the name string to extract context, type name, and type args+ -- (the same parser the top-level mkYesod entry points use).+ (namestr, mtys, appCxt') <- parseYesodNameQ nameStr -mkYesodSubDispatch :: [ResourceTree a] -> Q Exp-mkYesodSubDispatch res = do- clause' <- mkDispatchClause (mkMDS return [|subHelper|]) res- inner <- newName "inner"- let innerFun = FunD inner [clause']- helper <- newName "helper"- let fun = FunD helper- [ Clause- []- (NormalB $ VarE inner)- [innerFun]- ]- return $ LetE [fun] (VarE helper)+ appCxt <- buildAppCxt appCxt' -instanceD :: Cxt -> Type -> [Dec] -> Dec-instanceD = InstanceD Nothing+ foundation <- resolveFoundation namestr mtys resS+ -- The explicitly-written args plus the fresh vars filling the reified+ -- arity. Both are part of the subsite's full application ('rfSite'), so both+ -- must travel in the 'TyArgs' handed to 'discoveryMode' and the generators+ -- — otherwise a parameterized subsite invoked without explicit type args+ -- (e.g. @mkYesodSubDispatchInstance "MySub"@ for @MySub a@) would be+ -- misclassified as monomorphic and emit ill-scoped nested instances. This+ -- mirrors the 'fillNames' fix in 'mkYesodGeneralOpts'.+ let fillNames = fmap (\v -> (VarT v, v)) (rfFillVars foundation)+ tyArgs = toTyArgs (rfBoundNames foundation ++ fillNames)++ -- Generate the YesodSubDispatch instance+ masterN <- newName "master"+ let masterT = VarT masterN+ -- Thread the opts (in particular 'roNestedRouteFallthrough') into the+ -- generated @yesodSubDispatch@ body so a subsite's own top-level parent+ -- clauses honor 'setNestedRouteFallthrough', matching the top-level+ -- 'mkTopLevelDispatchInstance' path. 'GeneratesNestedInstances': this+ -- entry point emits a 'YesodSubDispatchNested' instance per parent in+ -- this same splice (below), so the flat @yesodSubDispatch@ body+ -- delegates each parent to that instance instead of inlining the+ -- subtree dispatch (which the nested instance would re-emit), avoiding+ -- the doubled codegen. This mirrors 'mkTopLevelDispatchInstance' on the+ -- top-level path. Standalone 'mkYesodSubDispatch'/'mkYesodSubDispatchWith'+ -- stay at 'NoSameSpliceNestedInstances'.+ subDispatchExp =+ mkYesodSubDispatchWithDelegate+ GeneratesNestedInstances+ opts+ (rfResources foundation)+ subDispatchBody <- subDispatchExp+ let yesodSubDispatchInst = instanceD+ appCxt+ (ConT ''YesodSubDispatch `AppT` rfSite foundation `AppT` masterT)+ [ FunD 'yesodSubDispatch+ [ Clause [] (NormalB subDispatchBody) [] ]+ ]++ -- The top-level nested parents, each of which gets a+ -- YesodSubDispatchNested instance generated below.+ let nestedNames = [ n | ResourceParent n _ _ _ _ <- rfResources foundation ]++ nestedInstances <- fmap mconcat $ forM nestedNames $ \nestedName -> do+ -- Resolve the nested datatype once and reuse it for both the+ -- "instance already exists?" probe and the arity check below.+ -- 'nestedInstanceExists' saturates by the datatype's own reified arity,+ -- so it can't throw a kind error on an arity-mismatched in-scope+ -- datatype — which is exactly the misuse the 'checkNestedSubArity'+ -- below diagnoses. That keeps the friendly arity check reachable+ -- instead of being preempted by a cryptic kind error from the probe.+ rc <- resolveRouteCon nestedName+ instanceExists <- nestedInstanceExists ''YesodSubDispatchNested rc+ if instanceExists+ then pure []+ else do+ -- This API targets parameterized subsites whose nested subroute+ -- datatypes carry the parent's type arguments (the subsite's+ -- 'tyArgs' are applied to the nested datatype directly below).+ -- Guard the misuse where the+ -- subsite is parameterized but the nested datatype is not:+ -- otherwise applying the subsite's type args to a kind-'Type'+ -- datatype produces a cryptic kind error from generated code.+ -- Only check when the datatype actually resolved — an+ -- unresolved name has no knowable arity (defaulting it to 0+ -- would wrongly report "0 type parameter(s)"), and downstream+ -- codegen reports the not-in-scope case on its own.+ assertNestedSubArity+ SubsiteCall+ (SubsiteName namestr)+ (SubsiteArity (tyArgsArity tyArgs))+ rc+ -- Call the worker directly with the foundation's+ -- already-parsed 'Type' resources, rather than re-parsing the+ -- 'String' resources through the public 'mkNestedSubDispatchInstance'.+ -- The generator applies 'tyArgs' to the nested datatype directly+ -- and never consults 'roParameterizedSubroute' (it only reads+ -- 'roNestedRouteFallthrough'), so passing 'opts' through+ -- unchanged is correct — forcing 'setParameterizedSubroute' here+ -- was a no-op.+ mkNestedDispatchInstanceWith SubsiteNested Nothing+ opts+ nestedName appCxt tyArgs return (rfResources foundation)++ return $ yesodSubDispatchInst : nestedInstances
src/Yesod/Core/Internal/Util.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} module Yesod.Core.Internal.Util ( putTime , getTime
src/Yesod/Core/Json.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE TypeSynonymInstances, OverloadedStrings #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-}+ module Yesod.Core.Json ( -- * Convert from a JSON value defaultLayoutJson@@ -32,6 +32,9 @@ , jsonOrRedirect , jsonEncodingOrRedirect , acceptsJson++ -- * Checking if data is JSON+ , contentTypeHeaderIsJson ) where import Yesod.Core.Handler (HandlerFor, getRequest, invalidArgs, redirect, selectRep, provideRep, rawRequestBody, ProvidedRep, lookupHeader)@@ -134,8 +137,8 @@ parseCheckJsonBody :: (MonadHandler m, J.FromJSON a) => m (J.Result a) parseCheckJsonBody = do mct <- lookupHeader "content-type"- case fmap (B8.takeWhile (/= ';')) mct of- Just "application/json" -> parseInsecureJsonBody+ case fmap contentTypeHeaderIsJson mct of+ Just True -> parseInsecureJsonBody _ -> return $ J.Error $ "Non-JSON content type: " ++ show mct -- | Same as 'parseInsecureJsonBody', but return an invalid args response on a parse@@ -218,3 +221,12 @@ . listToMaybe . reqAccept) `liftM` getRequest++-- | Given the @Content-Type@ header, returns if it is JSON.+--+-- This function is currently a simple check for @application/json@, but in the future may check for+-- alternative representations such as @<https://tools.ietf.org/html/rfc6839#section-3.1 xxx/yyy+json>@.+--+-- @since 1.6.17+contentTypeHeaderIsJson :: B8.ByteString -> Bool+contentTypeHeaderIsJson bs = B8.takeWhile (/= ';') bs == "application/json"
src/Yesod/Core/TypeCache.hs view
src/Yesod/Core/Types.hs view
@@ -1,16 +1,25 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE GADTs #-}-module Yesod.Core.Types where+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveFunctor #-} +module Yesod.Core.Types (+ module Yesod.Core.Types+ , module Yesod.Core.Types.ErrorResponse+ , module Yesod.Core.Types.Content+ , module Yesod.Core.Types.TypedContent+ , module Yesod.Core.Types.HandlerContents++ , module Yesod.Core.Internal.Util+ , module Yesod.Routes.Class+ , module Yesod.Core.TypeCache+ ) where+ import Data.Aeson (ToJSON) import qualified Data.ByteString.Builder as BB import Control.Arrow (first)@@ -29,16 +38,16 @@ import Data.Map (Map, unionWith) import qualified Data.Map as Map import Data.Monoid (Endo (..), Last (..))-import Data.Semigroup (Semigroup(..)) import Data.Serialize (Serialize (..), putByteString) import Data.String (IsString (fromString)) import Data.Text (Text) import qualified Data.Text as T+import Data.Text.Encoding import qualified Data.Text.Lazy.Builder as TBuilder import Data.Time (UTCTime)-import Data.Typeable (Typeable) import GHC.Generics (Generic)+import qualified GHC.Int as I import Language.Haskell.TH.Syntax (Loc) import qualified Network.HTTP.Types as H import Network.Wai (FilePart,@@ -57,8 +66,13 @@ import Control.DeepSeq (NFData (rnf)) import Yesod.Core.TypeCache (TypeMap, KeyedTypeMap) import Control.Monad.Logger (MonadLoggerIO (..))-import UnliftIO (MonadUnliftIO (..), UnliftIO (..))+import UnliftIO (MonadUnliftIO (..), SomeException) +import Yesod.Core.Types.ErrorResponse+import Yesod.Core.Types.Content+import Yesod.Core.Types.TypedContent+import Yesod.Core.Types.HandlerContents+ -- Sessions type SessionMap = Map Text ByteString @@ -184,6 +198,11 @@ -- -- Since 1.2.0 , rheMaxExpires :: !Text++ -- | @since 1.6.24.0+ -- catch function for rendering 500 pages on exceptions.+ -- by default this is catch from unliftio (rethrows all async exceptions).+ , rheCatchHandlerExceptions :: !(forall a m . MonadUnliftIO m => m a -> (SomeException -> m a) -> m a) } data HandlerData child site = HandlerData@@ -198,7 +217,13 @@ , yreSite :: !site , yreSessionBackend :: !(Maybe SessionBackend) , yreGen :: !(IO Int)- -- ^ Generate a random number+ -- ^ Generate a random number uniformly distributed in the full+ -- range of 'Int'.+ --+ -- Note: Before 1.6.20, the default value generates pseudo-random+ -- number in an unspecified range. The range size may not be a power+ -- of 2. Since 1.6.20, the default value uses a secure entropy source+ -- and generates in the full range of 'Int'. , yreGetMaxExpires :: !(IO Text) } @@ -233,7 +258,7 @@ -- | An extension of the basic WAI 'W.Application' datatype to provide extra -- features needed by Yesod. Users should never need to use this directly, as--- the 'HandlerT' monad and template haskell code should hide it away.+-- the 'HandlerFor' monad and template haskell code should hide it away. type YesodApp = YesodRequest -> ResourceT IO YesodResponse -- | A generic widget, allowing specification of both the subsite and master@@ -251,9 +276,6 @@ instance a ~ () => Monoid (WidgetFor site a) where mempty = return ()-#if !(MIN_VERSION_base(4,11,0))- mappend = (<>)-#endif instance a ~ () => Semigroup (WidgetFor site a) where x <> y = x >> y @@ -285,26 +307,18 @@ -- -- > PageContent url -> HtmlUrl url data PageContent url = PageContent- { pageTitle :: !Html- , pageHead :: !(HtmlUrl url)- , pageBody :: !(HtmlUrl url)+ { pageTitle :: !Html+ , pageDescription :: !(Maybe Text)+ , pageHead :: !(HtmlUrl url)+ , pageBody :: !(HtmlUrl url) } -data Content = ContentBuilder !BB.Builder !(Maybe Int) -- ^ The content and optional content length.- | ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ())- | ContentFile !FilePath !(Maybe FilePart)- | ContentDontEvaluate !Content--data TypedContent = TypedContent !ContentType !Content- type RepHtml = Html {-# DEPRECATED RepHtml "Please use Html instead" #-} newtype RepJson = RepJson Content newtype RepPlain = RepPlain Content newtype RepXml = RepXml Content -type ContentType = ByteString -- FIXME Text?- -- | Wrapper around types so that Handlers can return a domain type, even when -- the data will eventually be encoded as JSON. -- Example usage in a type signature:@@ -325,17 +339,6 @@ -- Since 1.1.0 newtype DontFullyEvaluate a = DontFullyEvaluate { unDontFullyEvaluate :: a } --- | Responses to indicate some form of an error occurred.-data ErrorResponse =- NotFound- | InternalError !Text- | InvalidArgs ![Text]- | NotAuthenticated- | PermissionDenied !Text- | BadMethod !H.Method- deriving (Show, Eq, Typeable, Generic)-instance NFData ErrorResponse- ----- header stuff -- | Headers to be added to a 'Result'. data Header =@@ -366,21 +369,19 @@ data Stylesheet url = Stylesheet { styleLocation :: !(Location url), styleAttributes :: ![(Text, Text)] } deriving (Show, Eq) newtype Title = Title { unTitle :: Html }+newtype Description = Description { unDescription :: Text } newtype Head url = Head (HtmlUrl url)- deriving Monoid-instance Semigroup (Head url) where- (<>) = mappend+ deriving (Semigroup, Monoid) newtype Body url = Body (HtmlUrl url)- deriving Monoid-instance Semigroup (Body url) where- (<>) = mappend+ deriving (Semigroup, Monoid) type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> TBuilder.Builder data GWData a = GWData { gwdBody :: !(Body a) , gwdTitle :: !(Last Title)+ , gwdDescription :: !(Last Description) , gwdScripts :: !(UniqueList (Script a)) , gwdStylesheets :: !(UniqueList (Stylesheet a)) , gwdCss :: !(Map (Maybe Text) (CssBuilderUrl a)) -- media type@@ -388,40 +389,18 @@ , gwdHead :: !(Head a) } instance Monoid (GWData a) where- mempty = GWData mempty mempty mempty mempty mempty mempty mempty-#if !(MIN_VERSION_base(4,11,0))- mappend = (<>)-#endif+ mempty = GWData mempty mempty mempty mempty mempty mempty mempty mempty instance Semigroup (GWData a) where- GWData a1 a2 a3 a4 a5 a6 a7 <>- GWData b1 b2 b3 b4 b5 b6 b7 = GWData+ GWData a1 a2 a3 a4 a5 a6 a7 a8 <>+ GWData b1 b2 b3 b4 b5 b6 b7 b8 = GWData (mappend a1 b1) (mappend a2 b2) (mappend a3 b3) (mappend a4 b4)- (unionWith mappend a5 b5)- (mappend a6 b6)+ (mappend a5 b5)+ (unionWith mappend a6 b6) (mappend a7 b7)--data HandlerContents =- HCContent !H.Status !TypedContent- | HCError !ErrorResponse- | HCSendFile !ContentType !FilePath !(Maybe FilePart)- | HCRedirect !H.Status !Text- | HCCreated !Text- | HCWai !W.Response- | HCWaiApp !W.Application- deriving Typeable--instance Show HandlerContents where- show (HCContent status (TypedContent t _)) = "HCContent " ++ show (status, t)- show (HCError e) = "HCError " ++ show e- show (HCSendFile ct fp mfp) = "HCSendFile " ++ show (ct, fp, mfp)- show (HCRedirect s t) = "HCRedirect " ++ show (s, t)- show (HCCreated t) = "HCCreated " ++ show t- show (HCWai _) = "HCWai"- show (HCWaiApp _) = "HCWaiApp"-instance Exception HandlerContents+ (mappend a8 b8) -- Instances for WidgetFor instance Applicative (WidgetFor site) where@@ -440,9 +419,8 @@ primitive = liftIO . primitive -- | @since 1.4.38 instance MonadUnliftIO (WidgetFor site) where- {-# INLINE askUnliftIO #-}- askUnliftIO = WidgetFor $ \wd ->- return (UnliftIO (flip unWidgetFor wd))+ {-# INLINE withRunInIO #-}+ withRunInIO inner = WidgetFor $ \x -> inner $ flip unWidgetFor x instance MonadReader (WidgetData site) (WidgetFor site) where ask = WidgetFor return local f (WidgetFor g) = WidgetFor $ g . f@@ -460,7 +438,7 @@ instance MonadLoggerIO (WidgetFor site) where askLoggerIO = WidgetFor $ return . rheLog . handlerEnv . wdHandler --- Instances for HandlerT+-- Instances for HandlerFor instance Applicative (HandlerFor site) where pure = HandlerFor . const . return (<*>) = ap@@ -479,9 +457,8 @@ -- | @since 1.4.38 instance MonadUnliftIO (HandlerFor site) where- {-# INLINE askUnliftIO #-}- askUnliftIO = HandlerFor $ \r ->- return (UnliftIO (flip unHandlerFor r))+ {-# INLINE withRunInIO #-}+ withRunInIO inner = HandlerFor $ \x -> inner $ flip unHandlerFor x instance MonadThrow (HandlerFor site) where throwM = liftIO . throwM@@ -498,9 +475,6 @@ instance Monoid (UniqueList x) where mempty = UniqueList id-#if !(MIN_VERSION_base(4,11,0))- mappend = (<>)-#endif instance Semigroup (UniqueList x) where UniqueList x <> UniqueList y = UniqueList $ x . y @@ -552,9 +526,8 @@ -- | @since 1.4.38 instance MonadUnliftIO (SubHandlerFor child master) where- {-# INLINE askUnliftIO #-}- askUnliftIO = SubHandlerFor $ \r ->- return (UnliftIO (flip unSubHandlerFor r))+ {-# INLINE withRunInIO #-}+ withRunInIO inner = SubHandlerFor $ \x -> inner $ flip unSubHandlerFor x instance MonadThrow (SubHandlerFor child master) where throwM = liftIO . throwM
+ src/Yesod/Core/Types/Content.hs view
@@ -0,0 +1,21 @@+module Yesod.Core.Types.Content where++import qualified Data.ByteString.Builder as BB+import Control.Monad.Trans.Resource (ResourceT)+import Data.Conduit (Flush, ConduitT)+import Network.Wai (FilePart)++data Content+ = ContentBuilder !BB.Builder !(Maybe Int)+ -- ^ The content and optional content length.+ --+ -- Note that, despite @Builder@'s laziness, this is entirely forced+ -- into memory by default in order to catch imprecise exceptions+ -- before beginning to respond. If you are confident you don't have+ -- imprecise exceptions, you may disable this by wrapping the+ -- `ToContent` data in `DontFullyEvaluate`.+ | ContentSource !(ConduitT () (Flush BB.Builder) (ResourceT IO) ())+ | ContentFile !FilePath !(Maybe FilePart)+ | ContentDontEvaluate !Content+ -- ^ Used internally to wrap @ContentBuilder@s to disable forcing+ -- them. No effect on other @Content@.
+ src/Yesod/Core/Types/ErrorResponse.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DeriveGeneric #-}++module Yesod.Core.Types.ErrorResponse where++import GHC.Generics (Generic)+import Data.Text+import Control.DeepSeq (NFData)+import qualified Network.HTTP.Types as H++-- | Responses to indicate some form of an error occurred.+data ErrorResponse =+ NotFound+ -- ^ The requested resource was not found.+ -- Examples of when this occurs include when an incorrect URL is used, or @yesod-persistent@'s 'get404' doesn't find a value.+ -- HTTP status: 404.+ | InternalError !Text+ -- ^ Some sort of unexpected exception.+ -- If your application uses `throwIO` or `error` to throw an exception, this is the form it would take.+ -- HTTP status: 500.+ | InvalidArgs ![Text]+ -- ^ Indicates some sort of invalid or missing argument, like a missing query parameter or malformed JSON body.+ -- Examples Yesod functions that send this include 'requireCheckJsonBody' and @Yesod.Auth.GoogleEmail2@.+ -- HTTP status: 400.+ | NotAuthenticated+ -- ^ Indicates the user is not logged in.+ -- This is thrown when 'isAuthorized' returns 'AuthenticationRequired'.+ -- HTTP code: 401.+ | PermissionDenied !Text+ -- ^ Indicates the user doesn't have permission to access the requested resource.+ -- This is thrown when 'isAuthorized' returns 'Unauthorized'.+ -- HTTP code: 403.+ | BadMethod !H.Method+ -- ^ Indicates the URL would have been valid if used with a different HTTP method (e.g. a GET was used, but only POST is handled.)+ -- HTTP code: 405.+ deriving (Show, Eq, Generic)++instance NFData ErrorResponse
+ src/Yesod/Core/Types/HandlerContents.hs view
@@ -0,0 +1,39 @@+module Yesod.Core.Types.HandlerContents+ (+ HandlerContents (..)+ ) where++import Control.Exception (Exception)+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Network.HTTP.Types as H+import qualified Network.Wai as W+import Yesod.Core.Types.ErrorResponse+import Yesod.Core.Types.TypedContent (ContentType, TypedContent (..), typedContentToSnippet)++data HandlerContents =+ HCContent !H.Status !TypedContent+ | HCError !ErrorResponse+ | HCSendFile !ContentType !FilePath !(Maybe W.FilePart)+ | HCRedirect !H.Status !T.Text+ | HCCreated !T.Text+ | HCWai !W.Response+ | HCWaiApp !W.Application++instance Show HandlerContents where+ show (HCContent status tc@(TypedContent t _))+ = mconcat [ "HCContent "+ , show (status, t)+ , " ("+ , fromMaybe "" $ TL.unpack <$> typedContentToSnippet tc 1000+ , ")"+ ]+ show (HCError e) = "HCError " ++ show e+ show (HCSendFile ct fp mfp) = "HCSendFile " ++ show (ct, fp, mfp)+ show (HCRedirect s t) = "HCRedirect " ++ show (s, t)+ show (HCCreated t) = "HCCreated " ++ show t+ show (HCWai _) = "HCWai"+ show (HCWaiApp _) = "HCWaiApp"++instance Exception HandlerContents
+ src/Yesod/Core/Types/TypedContent.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module Yesod.Core.Types.TypedContent+ (+ ContentType+ , TypedContent (..)+ , typedContentToSnippet+ ) where++import Control.Applicative ((<|>))+import Control.Monad (void, guard)+import Data.Maybe (fromMaybe)++import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Builder as BB++import qualified Data.Int as I++#if MIN_VERSION_text(2,1,0)+import qualified Data.Text.Encoding as TE (decodeASCIIPrefix)+#else+import qualified Data.Text.Encoding as TE (decodeLatin1)+#endif+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as LE (decodeUtf8With, decodeLatin1)+import qualified Data.Text.Encoding.Error as EE (lenientDecode)++import qualified Data.Encoding as Enc+import qualified Data.Encoding.GB18030 as Enc+import qualified Data.Encoding.CP1251 as Enc+import qualified Data.Encoding.ShiftJIS as Enc+import qualified Data.Encoding.CP932 as Enc++import qualified Network.Wai.Parse as NWP++import Yesod.Core.Types.Content (Content (..))++type ContentType = B.ByteString -- FIXME Text?+data TypedContent = TypedContent !ContentType !Content++decoderForCharset :: Maybe B.ByteString -> L.ByteString -> TL.Text+decoderForCharset (Just encodingSymbol)+ | encodingSymbol == "utf-8" =+ LE.decodeUtf8With EE.lenientDecode+ | encodingSymbol == "US-ASCII" =+#if MIN_VERSION_text(2,1,0)+ TL.fromStrict . fst . TE.decodeASCIIPrefix . L.toStrict+#else+ TL.fromStrict . TE.decodeLatin1 . L.toStrict+#endif+ | encodingSymbol == "latin1" =+ LE.decodeLatin1+ | encodingSymbol == "GB18030" =+ TL.pack . Enc.decodeLazyByteString Enc.GB18030+ | encodingSymbol == "windows-1251" =+ TL.pack . Enc.decodeLazyByteString Enc.CP1251+ | encodingSymbol == "Shift_JIS" =+ TL.pack . Enc.decodeLazyByteString Enc.ShiftJIS+ | encodingSymbol == "Windows-31J" =+ TL.pack . Enc.decodeLazyByteString Enc.CP932+ | otherwise =+ LE.decodeUtf8With EE.lenientDecode+decoderForCharset Nothing = LE.decodeUtf8With EE.lenientDecode++decodeForContentType :: ContentType -> L.ByteString -> Maybe TL.Text+decodeForContentType ct bytes = do+ let (t, params) =+ NWP.parseContentType ct+ charset =+ lookup "charset" params+ typeIsText =+ B.isPrefixOf "text" t+ || B.isPrefixOf "application/json" t+ || B.isPrefixOf "application/rss" t+ || B.isPrefixOf "application/atom" t+ decoder = decoderForCharset charset+ void charset <|> guard typeIsText+ pure $ decoder bytes++contentToSnippet :: Content -> I.Int64 -> Maybe L.ByteString+contentToSnippet (ContentBuilder builder maybeLength) maxLength =+ pure $ truncatedText <> excessLengthMsg+ where+ truncatedText = L.take maxLength $ BB.toLazyByteString builder+ excessLength = fromMaybe 0 $ (subtract $ fromIntegral maxLength) <$> maybeLength+ excessLengthMsg = case excessLength > 0 of+ False -> ""+ True -> "...+ " <> BB.toLazyByteString (BB.intDec excessLength)+contentToSnippet (ContentSource _) _ = Nothing+contentToSnippet (ContentFile _ _) _ = Nothing+contentToSnippet (ContentDontEvaluate _) _ = Nothing++-- | Represents TypedContent as a String, rendering at most a specified number of+-- bytes of the content, and annotating it with the remaining length. Returns Nothing+-- if the content type indicates the content is binary data.+--+-- @since 1.6.28.0+typedContentToSnippet :: TypedContent -> I.Int64 -> Maybe TL.Text+typedContentToSnippet (TypedContent t c) maxLength = decodeForContentType t =<< contentToSnippet c maxLength
src/Yesod/Core/Widget.hs view
@@ -1,13 +1,16 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE UndecidableInstances #-}+ -- | Widgets combine HTML with JS and CSS dependencies with a unique identifier -- generator, allowing you to create truly modular HTML components. module Yesod.Core.Widget@@ -29,6 +32,12 @@ -- ** Head of page , setTitle , setTitleI+ , setDescription+ , setDescriptionI+ , setDescriptionIdemp+ , setDescriptionIdempI+ , setOGType+ , setOGImage -- ** CSS , addStylesheet , addStylesheetAttrs@@ -49,7 +58,7 @@ , asWidgetT ) where -import Data.Monoid+import Data.Monoid (Last (..)) import qualified Text.Blaze.Html5 as H import Text.Hamlet import Text.Cassius@@ -58,6 +67,7 @@ import Yesod.Core.Handler (getMessageRender, getUrlRenderParams) import Text.Shakespeare.I18N (RenderMessage) import Data.Text (Text)+import Data.Kind (Type) import qualified Data.Map as Map import Language.Haskell.TH.Quote (QuasiQuoter) import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE, AppE), Pat (VarP), newName)@@ -71,7 +81,7 @@ import Yesod.Core.Types import Yesod.Core.Class.Handler -type WidgetT site (m :: * -> *) = WidgetFor site+type WidgetT site (m :: Type -> Type) = WidgetFor site {-# DEPRECATED WidgetT "Use WidgetFor directly" #-} preEscapedLazyText :: TL.Text -> Html@@ -81,19 +91,19 @@ toWidget :: (MonadWidget m, HandlerSite m ~ site) => a -> m () instance render ~ RY site => ToWidget site (render -> Html) where- toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty+ toWidget x = tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty mempty instance render ~ RY site => ToWidget site (render -> Css) where toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . x instance ToWidget site Css where toWidget x = toWidget $ CssBuilder . fromLazyText . renderCss . const x instance render ~ RY site => ToWidget site (render -> CssBuilder) where- toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty+ toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . x) mempty mempty instance ToWidget site CssBuilder where- toWidget x = tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . const x) mempty mempty+ toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton Nothing $ unCssBuilder . const x) mempty mempty instance render ~ RY site => ToWidget site (render -> Javascript) where- toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just x) mempty+ toWidget x = tell $ GWData mempty mempty mempty mempty mempty mempty (Just x) mempty instance ToWidget site Javascript where- toWidget x = tell $ GWData mempty mempty mempty mempty mempty (Just $ const x) mempty+ toWidget x = tell $ GWData mempty mempty mempty mempty mempty mempty (Just $ const x) mempty instance (site' ~ site, a ~ ()) => ToWidget site' (WidgetFor site a) where toWidget = liftWidget instance ToWidget site Html where@@ -124,9 +134,9 @@ instance ToWidgetMedia site Css where toWidgetMedia media x = toWidgetMedia media $ CssBuilder . fromLazyText . renderCss . const x instance render ~ RY site => ToWidgetMedia site (render -> CssBuilder) where- toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty+ toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . x) mempty mempty instance ToWidgetMedia site CssBuilder where- toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . const x) mempty mempty+ toWidgetMedia media x = tell $ GWData mempty mempty mempty mempty mempty (Map.singleton (Just media) $ unCssBuilder . const x) mempty mempty class ToWidgetBody site a where toWidgetBody :: (MonadWidget m, HandlerSite m ~ site) => a -> m ()@@ -144,7 +154,7 @@ toWidgetHead :: (MonadWidget m, HandlerSite m ~ site) => a -> m () instance render ~ RY site => ToWidgetHead site (render -> Html) where- toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty . Head+ toWidgetHead = tell . GWData mempty mempty mempty mempty mempty mempty mempty . Head instance render ~ RY site => ToWidgetHead site (render -> Css) where toWidgetHead = toWidget instance ToWidgetHead site Css where@@ -160,18 +170,133 @@ instance ToWidgetHead site Html where toWidgetHead = toWidgetHead . const --- | Set the page title. Calling 'setTitle' multiple times overrides previously--- set values.+-- | Set the page title.+--+-- Calling @setTitle@ or @setTitleI@ multiple times overrides previously set+-- values.+--+-- SEO Notes:+--+-- * Title tags are the second most important on-page factor for SEO, after+-- content+-- * Every page should have a unique title tag+-- * Start your title tag with your main targeted keyword+-- * Don't stuff your keywords+-- * Google typically shows 55-64 characters, so aim to keep your title+-- length under 60 characters setTitle :: MonadWidget m => Html -> m ()-setTitle x = tell $ GWData mempty (Last $ Just $ Title x) mempty mempty mempty mempty mempty+setTitle x = tell $ GWData mempty (Last $ Just $ Title x) mempty mempty mempty mempty mempty mempty --- | Set the page title. Calling 'setTitle' multiple times overrides previously--- set values.+-- | Set the localised page title.+--+-- n.b. See comments for @setTitle@ setTitleI :: (MonadWidget m, RenderMessage (HandlerSite m) msg) => msg -> m () setTitleI msg = do mr <- getMessageRender setTitle $ toHtml $ mr msg +-- | Add description meta tag to the head of the page+--+-- Google does not use the description tag as a ranking signal, but the+-- contents of this tag will likely affect your click-through rate since it+-- shows up in search results.+--+-- The average length of the description shown in Google's search results is+-- about 160 characters on desktop, and about 130 characters on mobile, at time+-- of writing.+--+-- Source: https://www.advancedwebranking.com/blog/meta-tags-important-in-seo/+--+-- @since 1.6.18+setDescription :: MonadWidget m => Text -> m ()+setDescription description =+ toWidgetHead $ [hamlet|<meta name=description content=#{description}>|]++{-# WARNING setDescription+ [ "setDescription is not idempotent; we recommend setDescriptionIdemp instead"+ , "Multiple calls to setDescription will insert multiple meta tags in the page head."+ , "If you want an idempotent function, use setDescriptionIdemp - but if you do, you \+ \may need to change your layout to include pageDescription."+ ]+#-}++-- | Add translated description meta tag to the head of the page+--+-- n.b. See comments for @setDescription@.+--+-- @since 1.6.18+setDescriptionI+ :: (MonadWidget m, RenderMessage (HandlerSite m) msg)+ => msg -> m ()+setDescriptionI msg = do+ mr <- getMessageRender+ toWidgetHead $ [hamlet|<meta name=description content=#{mr msg}>|]++{-# WARNING setDescriptionI+ [ "setDescriptionI is not idempotent; we recommend setDescriptionIdempI instead"+ , "Multiple calls to setDescriptionI will insert multiple meta tags in the page head."+ , "If you want an idempotent function, use setDescriptionIdempI - but if you do, you \+ \may need to change your layout to include pageDescription."+ ]+#-}++-- | Add description meta tag to the head of the page+--+-- Google does not use the description tag as a ranking signal, but the+-- contents of this tag will likely affect your click-through rate since it+-- shows up in search results.+--+-- The average length of the description shown in Google's search results is+-- about 160 characters on desktop, and about 130 characters on mobile, at time+-- of writing.+--+-- Unlike 'setDescription', this version is *idempotent* - calling it multiple+-- times will result in only a single description meta tag in the head.+--+-- Source: https://www.advancedwebranking.com/blog/meta-tags-important-in-seo/+--+-- @since 1.6.23+setDescriptionIdemp :: MonadWidget m => Text -> m ()+setDescriptionIdemp description = tell $ GWData mempty mempty (Last $ Just $ Description description) mempty mempty mempty mempty mempty++-- | Add translated description meta tag to the head of the page+--+-- n.b. See comments for @setDescriptionIdemp@.+--+-- Unlike 'setDescriptionI', this version is *idempotent* - calling it multiple+-- times will result in only a single description meta tag in the head.+--+-- @since 1.6.23+setDescriptionIdempI+ :: (MonadWidget m, RenderMessage (HandlerSite m) msg)+ => msg -> m ()+setDescriptionIdempI msg = do+ mr <- getMessageRender+ setDescriptionIdemp $ mr msg++-- | Add OpenGraph type meta tag to the head of the page+--+-- See all available OG types here: https://ogp.me/#types+--+-- @since 1.6.18+setOGType :: MonadWidget m => Text -> m ()+setOGType a = toWidgetHead $ [hamlet|<meta property="og:type" content=#{a}>|]++-- | Add OpenGraph image meta tag to the head of the page+--+-- Best practices:+--+-- * Use custom images for shareable pages, e.g., homepage, articles, etc.+-- * Use your logo or any other branded image for the rest of your pages.+-- * Use images with a 1.91:1 ratio and minimum recommended dimensions of+-- 1200x630 for optimal clarity across all devices.+--+-- Source: https://ahrefs.com/blog/open-graph-meta-tags/+--+-- @since 1.6.18+setOGImage :: MonadWidget m => Text -> m ()+setOGImage a = toWidgetHead $ [hamlet|<meta property="og:image" content=#{a}>|]+ -- | Link to the specified local stylesheet. addStylesheet :: MonadWidget m => Route (HandlerSite m) -> m () addStylesheet = flip addStylesheetAttrs []@@ -181,7 +306,7 @@ => Route (HandlerSite m) -> [(Text, Text)] -> m ()-addStylesheetAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Local x) y) mempty mempty mempty+addStylesheetAttrs x y = tell $ GWData mempty mempty mempty mempty (toUnique $ Stylesheet (Local x) y) mempty mempty mempty -- | Link to the specified remote stylesheet. addStylesheetRemote :: MonadWidget m => Text -> m ()@@ -189,7 +314,7 @@ -- | Link to the specified remote stylesheet. addStylesheetRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m ()-addStylesheetRemoteAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Remote x) y) mempty mempty mempty+addStylesheetRemoteAttrs x y = tell $ GWData mempty mempty mempty mempty (toUnique $ Stylesheet (Remote x) y) mempty mempty mempty addStylesheetEither :: MonadWidget m => Either (Route (HandlerSite m)) Text@@ -207,7 +332,7 @@ -- | Link to the specified local script. addScriptAttrs :: MonadWidget m => Route (HandlerSite m) -> [(Text, Text)] -> m ()-addScriptAttrs x y = tell $ GWData mempty mempty (toUnique $ Script (Local x) y) mempty mempty mempty mempty+addScriptAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Script (Local x) y) mempty mempty mempty mempty -- | Link to the specified remote script. addScriptRemote :: MonadWidget m => Text -> m ()@@ -215,7 +340,7 @@ -- | Link to the specified remote script. addScriptRemoteAttrs :: MonadWidget m => Text -> [(Text, Text)] -> m ()-addScriptRemoteAttrs x y = tell $ GWData mempty mempty (toUnique $ Script (Remote x) y) mempty mempty mempty mempty+addScriptRemoteAttrs x y = tell $ GWData mempty mempty mempty (toUnique $ Script (Remote x) y) mempty mempty mempty mempty whamlet :: QuasiQuoter whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings
src/Yesod/Routes/Class.hs view
@@ -1,13 +1,23 @@-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+ module Yesod.Routes.Class ( RenderRoute (..) , ParseRoute (..) , RouteAttrs (..)+ , -- * For nested routes and splitting files+ RenderRouteNested (..)+ , ParseRouteNested (..)+ , RouteAttrsNested (..)+ , WithParentArgs (..) ) where import Data.Text (Text) import Data.Set (Set)+import Data.Kind (Type) class Eq (Route a) => RenderRoute a where -- | The <http://www.yesodweb.com/book/routing-and-handlers type-safe URLs> associated with a site argument.@@ -15,10 +25,97 @@ renderRoute :: Route a -> ([Text], [(Text, Text)]) -- ^ The path of the URL split on forward slashes, and a list of query parameters with their associated value. +-- | This datatype wraps up the 'ParentArgs' for a route fragment with the+-- route fragment itself. A route fragment with its parent arguments can+-- be rendered and dispatched on.+--+-- @since 1.7.0.0+data WithParentArgs a = WithParentArgs+ { theParentArgs :: ParentArgs a+ , parentArgsFor :: a+ }++deriving stock instance (Eq (ParentArgs a), Eq a) => Eq (WithParentArgs a)++instance (Eq (ParentArgs a), RenderRouteNested a) => RenderRouteNested (WithParentArgs a) where+ type ParentSite (WithParentArgs a) = ParentSite a+ type ParentArgs (WithParentArgs a) = ()++ renderRouteNested () (WithParentArgs parentArgs a) =+ renderRouteNested parentArgs a++-- | This class acts as a delegation class for 'RenderRoute' on nested+-- route fragments.+--+-- @since 1.7.0.0+class Eq a => RenderRouteNested a where+ -- | The site type for a given route fragment.+ --+ -- @since 1.7.0.0+ type ParentSite a :: Type++ -- | The 'ParentArgs' are the route fragments necessary to call the+ -- dispatched route that are not part of the route fragments used in+ -- parsing the route.+ --+ -- @since 1.7.0.0+ type ParentArgs a :: Type+ type ParentArgs a = ()++ -- | Render the fragment of the route. To form a complete route, you'll+ -- need to 'mappend' this with the result from the parent 'renderRoute'+ -- or 'renderRouteNested' call.+ --+ -- @since 1.7.0.0+ renderRouteNested :: ParentArgs a -> a -> ([Text], [(Text, Text)])++instance (RenderRoute site) => RenderRouteNested (Route site) where+ type ParentSite (Route site) = site+ type ParentArgs (Route site) = ()++ renderRouteNested () a = renderRoute a+ class RenderRoute a => ParseRoute a where parseRoute :: ([Text], [(Text, Text)]) -- ^ The path of the URL split on forward slashes, and a list of query parameters with their associated value. -> Maybe (Route a) +-- | Like 'RenderRouteNested', this acts as a delegation class for nested+-- route fragments.+--+-- Note that 'parseRouteNested' and 'renderRouteNested' are /not/ inverses, by+-- design. 'renderRouteNested' consumes the @'ParentArgs' a@ (the parent's+-- dynamic pieces) to produce a complete path, whereas 'parseRouteNested'+-- recovers only the fragment @a@ and never the 'ParentArgs' — the parent is+-- responsible for those. So @'parseRouteNested' p@ followed by+-- @'renderRouteNested' args@ round-trips the /fragment/ portion of @p@ only,+-- with @args@ supplied separately by the caller.+--+-- @since 1.7.0.0+class RenderRouteNested a => ParseRouteNested a where+ -- | Parse a route fragment from a path. Like 'parseRoute', but produces+ -- the fragment @a@ directly rather than a complete 'Route'; the parent is+ -- responsible for constructing the full route from the result.+ --+ -- @since 1.7.0.0+ parseRouteNested+ :: ([Text], [(Text, Text)])+ -- ^ The path of the URL split on forward slashes, and a list of query parameters with their associated value.+ --+ -- Unlike for 'parseRoute', this will not include URL fragments as+ -- part of parent routes. It is expected that parents will take the+ -- resulting @a@ and construct the parent from that.+ -> Maybe a+ class RenderRoute a => RouteAttrs a where routeAttrs :: Route a -> Set Text -- ^ A set of <http://www.yesodweb.com/book/route-attributes attributes associated with the route>.++-- | Like 'RenderRouteNested', this acts as a delegation class for nested+-- route fragments to provide 'RouteAttrs'.+--+-- @since 1.7.0.0+class RenderRouteNested a => RouteAttrsNested a where+ -- | Retrieve the 'RouteAttrs' for a given route fragment.+ --+ -- @since 1.7.0.0+ routeAttrsNested :: a -> Set Text
src/Yesod/Routes/Overlap.hs view
@@ -24,7 +24,7 @@ , fHasSuffix = hasSuffix $ ResourceLeaf r , fCheck = check && resourceCheck r }- go names pieces check (ResourceParent newname check' newpieces children) =+ go names pieces check (ResourceParent newname check' _attrs newpieces children) = concatMap (go names' pieces' (check && check')) children where names' = names . (newname:)@@ -57,10 +57,18 @@ piecesOverlap pieceX pieceY && overlaps xs ys suffixX suffixY piecesOverlap :: Piece t -> Piece t -> Bool--- Statics only match if they equal. Dynamics match with anything+-- Statics only match if they are equal. A dynamic piece is treated as+-- overlapping with anything: although at runtime a typed dynamic (e.g. an+-- 'Int') only matches inputs that parse to its type, the overlap check is+-- intentionally conservative and never inspects the dynamic-piece type — it is+-- parametric in @t@ (see 'findOverlapNames'). Reporting a possible overlap that+-- cannot actually occur is harmless; missing a real one would not be. piecesOverlap (Static x) (Static y) = x == y piecesOverlap _ _ = True +-- | The overlap check never inspects the dynamic-piece type, so this is+-- parametric in it (the reported names come from 'resourceName', always a+-- 'String'). findOverlapNames :: [ResourceTree t] -> [(String, String)] findOverlapNames = map go . findOverlapsF . filter fCheck . concatMap Yesod.Routes.Overlap.flatten
src/Yesod/Routes/Parse.hs view
@@ -1,17 +1,21 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE PatternGuards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -fno-warn-missing-fields #-} -- QuasiQuoter+ module Yesod.Routes.Parse ( parseRoutes , parseRoutesFile , parseRoutesNoCheck , parseRoutesFileNoCheck , parseType+ , parseTypeM , parseTypeTree , TypeTree (..) , dropBracket+ , dropBracketM , nameToType+ , isTvar ) where import Language.Haskell.TH.Syntax@@ -31,14 +35,20 @@ parseRoutes = QuasiQuoter { quoteExp = x } where x s = do- let res = resourcesFromString s+ res <- resourcesFromString s case findOverlapNames res of [] -> lift res- z -> error $ unlines $ "Overlapping routes: " : map show z+ z -> fail $ unlines $ "Overlapping routes: " : map show z +-- | Same as 'parseRoutes', but uses an external file instead of quasiquotation.+--+-- The recommended file extension is @.yesodroutes@. parseRoutesFile :: FilePath -> Q Exp parseRoutesFile = parseRoutesFileWith parseRoutes +-- | Same as 'parseRoutesNoCheck', but uses an external file instead of quasiquotation.+--+-- The recommended file extension is @.yesodroutes@. parseRoutesFileNoCheck :: FilePath -> Q Exp parseRoutesFileNoCheck = parseRoutesFileWith parseRoutesNoCheck @@ -57,20 +67,24 @@ -- | Same as 'parseRoutes', but performs no overlap checking. parseRoutesNoCheck :: QuasiQuoter parseRoutesNoCheck = QuasiQuoter- { quoteExp = lift . resourcesFromString+ { quoteExp = \s -> resourcesFromString s >>= lift } -- | Converts a multi-line string to a set of resources. See documentation for--- the format of this string. This is a partial function which calls 'error' on--- invalid input.-resourcesFromString :: String -> [ResourceTree String]+-- the format of this string. Runs in any 'MonadFail' (e.g. the 'Q' monad of a+-- splice) and reports malformed input through 'fail', so diagnostics are+-- attributed to the splice rather than surfacing as a raw 'error' call stack.+resourcesFromString :: MonadFail m => String -> m [ResourceTree String] resourcesFromString =- fst . parse 0 . filter (not . all (== ' ')) . foldr lineContinuations [] . lines . filter (/= '\r')+ fmap fst . parse 0 . filter (not . all (== ' ')) . foldr lineContinuations [] . lines . filter (/= '\r') where- parse _ [] = ([], [])+ parse _ [] = pure ([], []) parse indent (thisLine:otherLines)- | length spaces < indent = ([], thisLine : otherLines)- | otherwise = (this others, remainder)+ | length spaces < indent = pure ([], thisLine : otherLines)+ | otherwise = do+ (this, otherLines') <- classify+ (others, remainder) <- parse indent otherLines'+ pure (this others, remainder) where parseAttr ('!':x) = Just x parseAttr _ = Nothing@@ -86,54 +100,55 @@ go front (x:xs) = go (front . (x:)) xs spaces = takeWhile (== ' ') thisLine- (others, remainder) = parse indent otherLines'- (this, otherLines') =- case takeWhile (not . isPrefixOf "--") $ splitSpaces thisLine of+ classify = do+ toks <- splitSpaces thisLine+ case takeWhile (not . isPrefixOf "--") toks of (pattern:rest0) | Just (constr:rest) <- stripColonLast rest0- , Just attrs <- mapM parseAttr rest ->- let (children, otherLines'') = parse (length spaces + 1) otherLines- children' = addAttrs attrs children- (pieces, Nothing, check) = piecesFromStringCheck pattern- in ((ResourceParent constr check pieces children' :), otherLines'')- (pattern:constr:rest) ->- let (pieces, mmulti, check) = piecesFromStringCheck pattern- (attrs, rest') = takeAttrs rest- disp = dispatchFromString rest' mmulti- in ((ResourceLeaf (Resource constr pieces disp attrs check):), otherLines)- [] -> (id, otherLines)- _ -> error $ "Invalid resource line: " ++ thisLine+ , Just attrs <- mapM parseAttr rest -> do+ (children, otherLines'') <- parse (length spaces + 1) otherLines+ let children' = addAttrs attrs children+ (pieces, mmulti, check) <- piecesFromStringCheck pattern+ case mmulti of+ Nothing -> pure ()+ Just _ -> fail $ "Parent routes cannot have a multipiece: " ++ pattern+ pure ((ResourceParent constr check (Set.fromList attrs) pieces children' :), otherLines'')+ (pattern:constr:rest) -> do+ (pieces, mmulti, check) <- piecesFromStringCheck pattern+ let (attrs, rest') = takeAttrs rest+ disp <- dispatchFromString rest' mmulti+ pure ((ResourceLeaf (Resource constr pieces disp attrs check):), otherLines)+ [] -> pure (id, otherLines)+ _ -> fail $ "Invalid resource line: " ++ thisLine -- | Splits a string by spaces, as long as the spaces are not enclosed by curly brackets (not recursive).-splitSpaces :: String -> [String]-splitSpaces "" = []-splitSpaces str = - let (rest, piece) = parse $ dropWhile isSpace str in- piece:(splitSpaces rest)+splitSpaces :: MonadFail m => String -> m [String]+splitSpaces "" = pure []+splitSpaces str = do+ (rest, piece) <- parse $ dropWhile isSpace str+ rest' <- splitSpaces rest+ pure (piece : rest') - where - parse :: String -> ( String, String)- parse ('{':s) = fmap ('{':) $ parseBracket s- parse (c:s) | isSpace c = (s, [])- parse (c:s) = fmap (c:) $ parse s- parse "" = ("", "")+ where+ parse ('{':s) = do (r, p) <- parseBracket s; pure (r, '{':p)+ parse (c:s) | isSpace c = pure (s, [])+ parse (c:s) = do (r, p) <- parse s; pure (r, c:p)+ parse "" = pure ("", "") - parseBracket :: String -> ( String, String)- parseBracket ('{':_) = error $ "Invalid resource line (nested curly bracket): " ++ str- parseBracket ('}':s) = fmap ('}':) $ parse s- parseBracket (c:s) = fmap (c:) $ parseBracket s- parseBracket "" = error $ "Invalid resource line (unclosed curly bracket): " ++ str+ parseBracket ('{':_) = fail $ "Invalid resource line (nested curly bracket): " ++ str+ parseBracket ('}':s) = do (r, p) <- parse s; pure (r, '}':p)+ parseBracket (c:s) = do (r, p) <- parseBracket s; pure (r, c:p)+ parseBracket "" = fail $ "Invalid resource line (unclosed curly bracket): " ++ str -piecesFromStringCheck :: String -> ([Piece String], Maybe String, Bool)-piecesFromStringCheck s0 =- (pieces, mmulti, check)+piecesFromStringCheck :: MonadFail m => String -> m ([Piece String], Maybe String, Bool)+piecesFromStringCheck s0 = do+ let (s1, check1) = stripBang s0+ (pieces', mmulti') <- piecesFromString $ drop1Slash s1+ let pieces = map snd pieces'+ mmulti = fmap snd mmulti'+ check = check1 && all fst pieces' && maybe True fst mmulti'+ pure (pieces, mmulti, check) where- (s1, check1) = stripBang s0- (pieces', mmulti') = piecesFromString $ drop1Slash s1- pieces = map snd pieces'- mmulti = fmap snd mmulti'- check = check1 && all fst pieces' && maybe True fst mmulti'- stripBang ('!':rest) = (rest, False) stripBang x = (x, True) @@ -142,7 +157,7 @@ map goTree where goTree (ResourceLeaf res) = ResourceLeaf (goRes res)- goTree (ResourceParent w x y z) = ResourceParent w x y (map goTree z)+ goTree (ResourceParent v w x y z) = ResourceParent v w x y (map goTree z) goRes res = res { resourceAttrs = noDupes ++ resourceAttrs res }@@ -169,36 +184,46 @@ go x y (('!':attr):rest) = go (x . (attr:)) y rest go x y (z:rest) = go x (y . (z:)) rest -dispatchFromString :: [String] -> Maybe String -> Dispatch String+dispatchFromString :: MonadFail m => [String] -> Maybe String -> m (Dispatch String) dispatchFromString rest mmulti- | null rest = Methods mmulti []- | all (all isUpper) rest = Methods mmulti rest+ | null rest = pure $ Methods mmulti []+ | all (all isUpper) rest = pure $ Methods mmulti rest dispatchFromString [subTyp, subFun] Nothing =- Subsite subTyp subFun+ pure $ Subsite subTyp subFun dispatchFromString [_, _] Just{} =- error "Subsites cannot have a multipiece"-dispatchFromString rest _ = error $ "Invalid list of methods: " ++ show rest+ fail "Subsites cannot have a multipiece"+dispatchFromString rest _ = fail $ "Invalid list of methods: " ++ show rest drop1Slash :: String -> String drop1Slash ('/':x) = x drop1Slash x = x -piecesFromString :: String -> ([(CheckOverlap, Piece String)], Maybe (CheckOverlap, String))-piecesFromString "" = ([], Nothing)-piecesFromString x =+piecesFromString :: MonadFail m => String -> m ([(CheckOverlap, Piece String)], Maybe (CheckOverlap, String))+piecesFromString "" = pure ([], Nothing)+piecesFromString x = do+ rest <- piecesFromString $ drop 1 z+ this <- pieceFromString y case (this, rest) of- (Left typ, ([], Nothing)) -> ([], Just typ)- (Left _, _) -> error "Multipiece must be last piece"- (Right piece, (pieces, mtyp)) -> (piece:pieces, mtyp)+ (Left typ, ([], Nothing)) -> pure ([], Just typ)+ (Left _, _) -> fail "Multipiece must be last piece"+ (Right piece, (pieces, mtyp)) -> pure (piece:pieces, mtyp) where (y, z) = break (== '/') x- this = pieceFromString y- rest = piecesFromString $ drop 1 z parseType :: String -> Type parseType orig = maybe (error $ "Invalid type: " ++ show orig) ttToType $ parseTypeTree orig +-- | 'parseType' in 'MonadFail': a malformed type surfaces via 'fail' instead of+-- a raw 'error'. Used at the 'Language.Haskell.TH.Q' splice sites so a bad type+-- in a route definition becomes an attributed compile error. The pure+-- 'parseType' is retained for callers (e.g. tests) supplying known-good input.+--+-- @since 1.7.0.0+parseTypeM :: MonadFail m => String -> m Type+parseTypeM orig =+ maybe (fail $ "Invalid type: " ++ show orig) (pure . ttToType) $ parseTypeTree orig+ parseTypeTree :: String -> Maybe TypeTree parseTypeTree orig = toTypeTree pieces@@ -254,43 +279,62 @@ gos' (front . (t:)) xs' ttToType :: TypeTree -> Type-ttToType (TTTerm s) = nameToType s+ttToType (TTTerm s) = fst $ nameToType s ttToType (TTApp x y) = ttToType x `AppT` ttToType y ttToType (TTList t) = ListT `AppT` ttToType t -nameToType :: String -> Type-nameToType t@(h:_) | isLower h = VarT $ mkName t-nameToType t = ConT $ mkName t+nameToType :: String -> (Type, Name)+nameToType t = (, nm) $ if isTvar t+ then VarT nm+ else ConT nm+ where nm = mkName t -pieceFromString :: String -> Either (CheckOverlap, String) (CheckOverlap, Piece String)-pieceFromString ('#':'!':x) = Right $ (False, Dynamic $ dropBracket x)-pieceFromString ('!':'#':x) = Right $ (False, Dynamic $ dropBracket x) -- https://github.com/yesodweb/yesod/issues/652-pieceFromString ('#':x) = Right $ (True, Dynamic $ dropBracket x)+isTvar :: String -> Bool+isTvar (h:_) = isLower h+isTvar _ = False -pieceFromString ('*':'!':x) = Left (False, x)-pieceFromString ('+':'!':x) = Left (False, x)+-- | 'MonadFail' so that an unclosed bracket in a 'Dynamic' piece (via+-- 'dropBracketM') surfaces as an attributed splice error rather than a raw+-- 'error' (the pure 'dropBracket' would otherwise bottom out deep in codegen).+pieceFromString :: MonadFail m => String -> m (Either (CheckOverlap, String) (CheckOverlap, Piece String))+pieceFromString ('#':'!':x) = Right . (False,) . Dynamic <$> dropBracketM x+pieceFromString ('!':'#':x) = Right . (False,) . Dynamic <$> dropBracketM x -- https://github.com/yesodweb/yesod/issues/652+pieceFromString ('#':x) = Right . (True,) . Dynamic <$> dropBracketM x -pieceFromString ('!':'*':x) = Left (False, x)-pieceFromString ('!':'+':x) = Left (False, x)+pieceFromString ('*':'!':x) = pure $ Left (False, x)+pieceFromString ('+':'!':x) = pure $ Left (False, x) -pieceFromString ('*':x) = Left (True, x)-pieceFromString ('+':x) = Left (True, x)+pieceFromString ('!':'*':x) = pure $ Left (False, x)+pieceFromString ('!':'+':x) = pure $ Left (False, x) -pieceFromString ('!':x) = Right $ (False, Static x)-pieceFromString x = Right $ (True, Static x)+pieceFromString ('*':x) = pure $ Left (True, x)+pieceFromString ('+':x) = pure $ Left (True, x) +pieceFromString ('!':x) = pure $ Right (False, Static x)+pieceFromString x = pure $ Right (True, Static x)+ dropBracket :: String -> String dropBracket str@('{':x) = case break (== '}') x of (s, "}") -> s _ -> error $ "Unclosed bracket ('{'): " ++ str dropBracket x = x +-- | 'dropBracket' in 'MonadFail': an unclosed @{@ surfaces via 'fail' instead+-- of a raw 'error', so it becomes an attributed compile error at a splice site.+--+-- @since 1.7.0.0+dropBracketM :: MonadFail m => String -> m String+dropBracketM str@('{':x) = case break (== '}') x of+ (s, "}") -> pure s+ _ -> fail $ "Unclosed bracket ('{'): " ++ str+dropBracketM x = pure x+ -- | If this line ends with a backslash, concatenate it together with the next line. -- -- @since 1.6.8 lineContinuations :: String -> [String] -> [String] lineContinuations this [] = [this] lineContinuations this below@(next:rest) = case unsnoc this of- Just (this', '\\') -> (this'++next):rest+ Just (this', '\\') -> (this' ++ next):rest _ -> this:below where unsnoc s = if null s then Nothing else Just (init s, last s)
src/Yesod/Routes/TH.hs view
@@ -1,11 +1,40 @@ module Yesod.Routes.TH ( module Yesod.Routes.TH.Types -- * Functions- , module Yesod.Routes.TH.RenderRoute+ , -- ** RenderRoute+ mkRenderRouteInstanceOpts+ , mkRouteConsOpts+ , shouldCreateResources++ , RouteOpts+ , defaultOpts+ , setEqDerived+ , setShowDerived+ , setReadDerived+ , setFocusOnNestedRoute+ , unsetFocusOnNestedRoute+ , roFocusOnNestedRoute+ , roNestedRouteFallthrough+ , setCreateResources+ , setParameterizedSubroute+ , setNestedRouteFallthrough+ , DiscoveryMode(..)+ , discoveryMode , module Yesod.Routes.TH.ParseRoute , module Yesod.Routes.TH.RouteAttrs -- ** Dispatch- , module Yesod.Routes.TH.Dispatch+ , MkDispatchSettings (..)+ , mkDispatchClause+ , defaultGetHandler+ , SDC(..)+ , mkDispatchInstance+ , mkNestedDispatchInstance+ , mkNestedDispatchInstanceWith+ , NestedTarget (..)+ , mkMDS+ , mkYesodSubDispatch+ , mkYesodSubDispatchWith+ , subTopDispatch ) where import Yesod.Routes.TH.Types
src/Yesod/Routes/TH/Dispatch.hs view
@@ -1,202 +1,1054 @@-{-# LANGUAGE RecordWildCards, TemplateHaskell, ViewPatterns #-}-module Yesod.Routes.TH.Dispatch- ( MkDispatchSettings (..)- , mkDispatchClause- , defaultGetHandler- ) where--import Prelude hiding (exp)-import Language.Haskell.TH.Syntax-import Web.PathPieces-import Data.Maybe (catMaybes)-import Control.Monad (forM)-import Data.List (foldl')-import Control.Arrow (second)-import System.Random (randomRIO)-import Yesod.Routes.TH.Types-import Data.Char (toLower)--data MkDispatchSettings b site c = MkDispatchSettings- { mdsRunHandler :: Q Exp- , mdsSubDispatcher :: Q Exp- , mdsGetPathInfo :: Q Exp- , mdsSetPathInfo :: Q Exp- , mdsMethod :: Q Exp- , mds404 :: Q Exp- , mds405 :: Q Exp- , mdsGetHandler :: Maybe String -> String -> Q Exp- , mdsUnwrapper :: Exp -> Q Exp- }--data SDC = SDC- { clause404 :: Clause- , extraParams :: [Exp]- , extraCons :: [Exp]- , envExp :: Exp- , reqExp :: Exp- }---- | A simpler version of Yesod.Routes.TH.Dispatch.mkDispatchClause, based on--- view patterns.------ Since 1.4.0-mkDispatchClause :: MkDispatchSettings b site c -> [ResourceTree a] -> Q Clause-mkDispatchClause MkDispatchSettings {..} resources = do- suffix <- qRunIO $ randomRIO (1000, 9999 :: Int)- envName <- newName $ "env" ++ show suffix- reqName <- newName $ "req" ++ show suffix- helperName <- newName $ "helper" ++ show suffix-- let envE = VarE envName- reqE = VarE reqName- helperE = VarE helperName-- clause404' <- mkClause404 envE reqE- getPathInfo <- mdsGetPathInfo- let pathInfo = getPathInfo `AppE` reqE-- let sdc = SDC- { clause404 = clause404'- , extraParams = []- , extraCons = []- , envExp = envE- , reqExp = reqE- }- clauses <- mapM (go sdc) resources-- return $ Clause- [VarP envName, VarP reqName]- (NormalB $ helperE `AppE` pathInfo)- [FunD helperName $ clauses ++ [clause404']]- where- handlePiece :: Piece a -> Q (Pat, Maybe Exp)- handlePiece (Static str) = return (LitP $ StringL str, Nothing)- handlePiece (Dynamic _) = do- x <- newName "dyn"- let pat = ViewP (VarE 'fromPathPiece) (ConP 'Just [VarP x])- return (pat, Just $ VarE x)-- handlePieces :: [Piece a] -> Q ([Pat], [Exp])- handlePieces = fmap (second catMaybes . unzip) . mapM handlePiece-- mkCon :: String -> [Exp] -> Exp- mkCon name = foldl' AppE (ConE $ mkName name)-- mkPathPat :: Pat -> [Pat] -> Pat- mkPathPat final =- foldr addPat final- where- addPat x y = ConP '(:) [x, y]-- go :: SDC -> ResourceTree a -> Q Clause- go sdc (ResourceParent name _check pieces children) = do- (pats, dyns) <- handlePieces pieces- let sdc' = sdc- { extraParams = extraParams sdc ++ dyns- , extraCons = extraCons sdc ++ [mkCon name dyns]- }- childClauses <- mapM (go sdc') children-- restName <- newName "rest"- let restE = VarE restName- restP = VarP restName-- helperName <- newName $ "helper" ++ name- let helperE = VarE helperName-- return $ Clause- [mkPathPat restP pats]- (NormalB $ helperE `AppE` restE)- [FunD helperName $ childClauses ++ [clause404 sdc]]- go SDC {..} (ResourceLeaf (Resource name pieces dispatch _ _check)) = do- (pats, dyns) <- handlePieces pieces-- (chooseMethod, finalPat) <- handleDispatch dispatch dyns-- return $ Clause- [mkPathPat finalPat pats]- (NormalB chooseMethod)- []- where- handleDispatch :: Dispatch a -> [Exp] -> Q (Exp, Pat)- handleDispatch dispatch' dyns =- case dispatch' of- Methods multi methods -> do- (finalPat, mfinalE) <-- case multi of- Nothing -> return (ConP '[] [], Nothing)- Just _ -> do- multiName <- newName "multi"- let pat = ViewP (VarE 'fromPathMultiPiece)- (ConP 'Just [VarP multiName])- return (pat, Just $ VarE multiName)-- let dynsMulti =- case mfinalE of- Nothing -> dyns- Just e -> dyns ++ [e]- route' = foldl' AppE (ConE (mkName name)) dynsMulti- route = foldr AppE route' extraCons- jroute = ConE 'Just `AppE` route- allDyns = extraParams ++ dynsMulti- mkRunExp mmethod = do- runHandlerE <- mdsRunHandler- handlerE' <- mdsGetHandler mmethod name- handlerE <- mdsUnwrapper $ foldl' AppE handlerE' allDyns- return $ runHandlerE- `AppE` handlerE- `AppE` envExp- `AppE` jroute- `AppE` reqExp-- func <-- case methods of- [] -> mkRunExp Nothing- _ -> do- getMethod <- mdsMethod- let methodE = getMethod `AppE` reqExp- matches <- forM methods $ \method -> do- exp <- mkRunExp (Just method)- return $ Match (LitP $ StringL method) (NormalB exp) []- match405 <- do- runHandlerE <- mdsRunHandler- handlerE <- mds405- let exp = runHandlerE- `AppE` handlerE- `AppE` envExp- `AppE` jroute- `AppE` reqExp- return $ Match WildP (NormalB exp) []- return $ CaseE methodE $ matches ++ [match405]-- return (func, finalPat)- Subsite _ getSub -> do- restPath <- newName "restPath"- setPathInfoE <- mdsSetPathInfo- subDispatcherE <- mdsSubDispatcher- runHandlerE <- mdsRunHandler- sub <- newName "sub"- let allDyns = extraParams ++ dyns- sroute <- newName "sroute"- let sub2 = LamE [VarP sub]- (foldl' (\a b -> a `AppE` b) (VarE (mkName getSub) `AppE` VarE sub) allDyns)- let reqExp' = setPathInfoE `AppE` VarE restPath `AppE` reqExp- route' = foldl' AppE (ConE (mkName name)) dyns- route = LamE [VarP sroute] $ foldr AppE (AppE route' $ VarE sroute) extraCons- exp = subDispatcherE- `AppE` runHandlerE- `AppE` sub2- `AppE` route- `AppE` envExp- `AppE` reqExp'- return (exp, VarP restPath)-- mkClause404 envE reqE = do- handler <- mds404- runHandler <- mdsRunHandler- let exp = runHandler `AppE` handler `AppE` envE `AppE` ConE 'Nothing `AppE` reqE- return $ Clause [WildP] (NormalB exp) []--defaultGetHandler :: Maybe String -> String -> Q Exp-defaultGetHandler Nothing s = return $ VarE $ mkName $ "handle" ++ s-defaultGetHandler (Just method) s = return $ VarE $ mkName $ map toLower method ++ s+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ViewPatterns #-}++module Yesod.Routes.TH.Dispatch+ ( MkDispatchSettings (..)+ , mkDispatchClause+ , defaultGetHandler+ , SDC(..)+ , mkDispatchInstance+ , mkNestedDispatchInstance+ , mkNestedDispatchInstanceWith+ , NestedTarget (..)+ , SameSpliceNestedInstances (..)+ , mkMDS+ , mkYesodSubDispatch+ , mkYesodSubDispatchWith+ , mkYesodSubDispatchWithDelegate+ , subTopDispatch+ , parseYesodName+ ) where++import Text.Parsec (parse, many1, many, eof, try, option, sepBy1)+import Text.ParserCombinators.Parsec.Char (alphaNum, spaces, string, char)+import Data.Maybe+import Data.Proxy (Proxy(..))+import Yesod.Routes.TH.RenderRoute+import qualified Network.Wai as W+import Yesod.Core.Content (ToTypedContent (..))+import Language.Haskell.TH hiding (cxt, instanceD)+import Yesod.Core.Types hiding (Body)+import Yesod.Core.Class.Dispatch+import Prelude hiding (exp)+import Yesod.Routes.TH.Internal+import Control.Monad+import Control.Monad.Reader (ReaderT, runReaderT, asks, local, lift)+import Data.List (foldl')+import Yesod.Routes.TH.Types+import Data.Char (toLower)+import Yesod.Core.Internal.Run+import Yesod.Core.Handler+import Yesod.Core.Class.Dispatch.ToParentRoute (ToParentRoute(..))+import Yesod.Core.Class.Yesod (Yesod)++-- | This datatype describes how to create the dispatch clause for a route+-- path.+data MkDispatchSettings b site c = MkDispatchSettings+ { mdsRunHandler :: Q Exp+ , mdsSubDispatcher :: Q Exp+ , mdsGetPathInfo :: Q Exp+ , mdsSetPathInfo :: Q Exp+ , mdsMethod :: Q Exp+ , mds404 :: Q Exp+ , mds405 :: Q Exp+ , mdsGetHandler :: Maybe String -> String -> Q Exp+ , mdsUnwrapper :: Exp -> Q Exp+ , mdsNestedRouteFallthrough :: !Bool+ -- ^ When 'True', fall through if no route matches (except in the final+ -- case). When 'False', return 404 if the current route clause fails to+ -- match.+ --+ -- @since 1.7.0.0+ , mdsNestedTarget :: NestedTarget+ -- ^ Whether this dispatch is for a top-level site or a subsite. Selects the+ -- nested-dispatch class to probe ('nestedTargetClass') and the+ -- nested-dispatch function to delegate to ('nestedTargetFn'):+ -- 'TopLevelNested' uses @YesodDispatchNested@\/@yesodDispatchNested@,+ -- 'SubsiteNested' uses @YesodSubDispatchNested@\/@yesodSubDispatchNested@.+ --+ -- @since 1.7.0.0+ , mdsSameSpliceNestedInstances :: !SameSpliceNestedInstances+ -- ^ Whether the caller emits the matching per-parent nested-dispatch+ -- instances in the same splice as this flat dispatch body; see+ -- 'SameSpliceNestedInstances' for how this affects a parent clause.+ -- 'mkMDS' defaults to 'NoSameSpliceNestedInstances'.+ --+ -- @since 1.7.0.0+ }++data SDC = SDC+ { extraParams :: [Exp]+ , extraCons :: [Exp]+ , envExp :: Exp+ , reqExp :: Exp+ }++-- | Which phase of clause generation we're in, which is the only thing that+-- decides how a matched result is wrapped (see 'wrapForPhase'). At the top level+-- the dispatch result is returned unwrapped; inside a parent's inline children+-- we generate helper clauses whose enclosing parent must tell a match from a+-- fall-through miss, so the result is wrapped in 'Just'. Generation starts at+-- 'TopLevelPhase' and flips to 'NestedPhase' on the first descent (and never+-- back), so the two phases are a single 'local'.+data DispatchPhase = TopLevelPhase | NestedPhase++-- | How a matched result is wrapped in a given phase. See 'DispatchPhase'.+wrapForPhase :: DispatchPhase -> Exp -> Exp+wrapForPhase TopLevelPhase = id+wrapForPhase NestedPhase = \e -> ConE 'Just `AppE` e++-- | The reader environment threaded through 'mkDispatchClause's clause+-- generator. 'envSdc' is the accumulated dispatch context (extended via 'local'+-- as we descend into a parent's children); 'envPhase' selects how a matched+-- result is wrapped.+data Env = Env+ { envSdc :: SDC+ , envPhase :: DispatchPhase+ }++-- | The monad 'mkDispatchClause's clause generator runs in: 'Q' carrying an+-- 'Env' read through 'MonadReader'. Every helper that needs the dispatch+-- context or the result wrapper takes them from the environment ('asks' /+-- 'local') rather than as arguments; 'liftQ' lifts the concrete-'Q' primitives.+type DispatchM = ReaderT Env Q++-- | Whether nested-route dispatch is being generated for a top-level site or+-- for a subsite. This is the single bit that selects the whole top-vs-subsite+-- plumbing: the dispatch class to probe and the function to delegate to (used+-- by 'MkDispatchSettings' via 'mdsNestedTarget'), and the runner function,+-- dispatch function\/class, and subsite-environment construction used when+-- generating nested-dispatch instances. All of those follow from this enum via+-- the @nestedTarget*@ derivation functions below.+--+-- @since 1.7.0.0+data NestedTarget+ = TopLevelNested+ -- ^ A top-level site: @YesodDispatchNested@\/@yesodDispatchNested@, run via+ -- @yesodRunner@, constructing 'YesodSubRunnerEnv' directly.+ | SubsiteNested+ -- ^ A subsite: @YesodSubDispatchNested@\/@yesodSubDispatchNested@, run via+ -- @subHelper@, composing through the outer 'YesodSubRunnerEnv' (like+ -- 'subTopDispatch').+ deriving (Eq, Show)++-- | Whether the splice emitting a flat dispatch body also generates the+-- matching per-parent nested-dispatch instances (@YesodDispatchNested@ \/+-- @YesodSubDispatchNested@) in the same declaration group.+--+-- @since 1.7.0.0+data SameSpliceNestedInstances+ = GeneratesNestedInstances+ -- ^ The caller emits the per-parent nested instances alongside the flat+ -- dispatch, so a parent clause /delegates/ to its instance even though the+ -- instance does not exist at probe time — GHC resolves it once the whole+ -- declaration group is spliced. This avoids inlining each nested leaf's+ -- dispatch into the flat body /and/ re-emitting it in the per-parent+ -- instance (see 'mkTopLevelDispatchInstance').+ | NoSameSpliceNestedInstances+ -- ^ Only the flat dispatch is emitted. A parent delegates only when its+ -- nested instance already exists (the cross-module split case) and inlines+ -- otherwise — required for standalone entry points like+ -- 'mkYesodSubDispatch', where delegating would reference a missing+ -- instance.+ deriving (Eq, Show)++-- | The nested-dispatch class to probe\/emit for a target:+-- @''YesodDispatchNested@ for top-level, @''YesodSubDispatchNested@ for subsites.+nestedTargetClass :: NestedTarget -> Name+nestedTargetClass TopLevelNested = ''YesodDispatchNested+nestedTargetClass SubsiteNested = ''YesodSubDispatchNested++-- | The nested-dispatch function to delegate to\/emit for a target:+-- @'yesodDispatchNested@ for top-level, @'yesodSubDispatchNested@ for subsites.+nestedTargetFn :: NestedTarget -> Name+nestedTargetFn TopLevelNested = 'yesodDispatchNested+nestedTargetFn SubsiteNested = 'yesodSubDispatchNested++-- | The runner function used inside generated nested-dispatch clauses:+-- @'yesodRunner@ for top-level, @'subHelper@ for subsites.+nestedTargetRunner :: NestedTarget -> Name+nestedTargetRunner TopLevelNested = 'yesodRunner+nestedTargetRunner SubsiteNested = 'subHelper++-- | The 'ArityCallSite' for arity-mismatch error messages: a top-level site is+-- reached through @mkYesod@, a subsite through @mkYesodSubDispatchInstance@.+nestedTargetCallSite :: NestedTarget -> ArityCallSite+nestedTargetCallSite TopLevelNested = TopLevelCall+nestedTargetCallSite SubsiteNested = SubsiteCall++-- | A simpler version of Yesod.Routes.TH.Dispatch.mkDispatchClause, based on+-- view patterns.+--+-- The function returns the dispatch 'Clause' along with a @['String']@ of the+-- names of nested route types that require a delegation instance to be+-- generated. For 'YesodDispatch', those are 'YesodDispatchNested' instances; for+-- the subsite case, 'YesodSubDispatchNested'.+--+-- @since 1.7.0.0 — the leading @[Name]@\/@[Exp]@ parameters were dropped and the+-- result type changed from @Clause@ to @Q ([String], Clause)@.+mkDispatchClause :: forall a b site c. TyArgs -> MkDispatchSettings b site c -> [ResourceTree a] -> Q ([String], Clause)+mkDispatchClause tyargs MkDispatchSettings {..} resources = do+ envName <- newName "env"+ reqName <- newName "req"+ helperName <- newName "dispatchHelper"++ let envE = VarE envName+ reqE = VarE reqName+ helperE = VarE helperName++ clause404' <- mkClause404 envE reqE+ getPathInfo <- mdsGetPathInfo+ let pathInfo = getPathInfo `AppE` reqE++ let sdc = SDC+ { extraParams = []+ , extraCons = []+ , envExp = envE+ , reqExp = reqE+ }+ -- Generate the dispatch clauses. 'go' runs in @'ReaderT' 'Env' Q@: the+ -- top-level call starts at @envPhase = TopLevelPhase@ so the top-level+ -- resources produce the final (unwrapped) dispatch result and report which+ -- parents need a nested-dispatch instance generated. Descending into a+ -- parent's inline children flips @envPhase@ to 'NestedPhase' (via 'local'),+ -- so children are 'Just'-wrapped helper clauses the enclosing parent can+ -- fall through on; their reported names are dropped (only top-level parents+ -- matter).+ let topEnv = Env { envSdc = sdc, envPhase = TopLevelPhase }+ (childNames, clauses) <- mconcat <$> runReaderT (mapM go resources) topEnv++ pure+ ( childNames+ , Clause+ [VarP envName, VarP reqName]+ (NormalB $ helperE `AppE` pathInfo)+ [FunD helperName $ clauses ++ [clause404']]+ )+ where+ -- Lift a concrete-'Q' action (the TH primitives and this package's own+ -- @Q@-typed helpers) into 'DispatchM'.+ liftQ :: Q x -> DispatchM x+ liftQ = lift++ -- The current phase's result wrapper (see 'wrapForPhase').+ askWrap :: DispatchM (Exp -> Exp)+ askWrap = asks (wrapForPhase . envPhase)++ -- Run an action in the scope of a parent's inline children: extend the+ -- accumulated dynamics and parent constructors with this parent's, and flip+ -- to 'NestedPhase'. This single 'local' — entered once at the top→nested+ -- boundary and never undone — is the entire top-vs-nested phase distinction.+ withChildScope :: [Exp] -> Exp -> DispatchM r -> DispatchM r+ withChildScope dyns constr = local $ \e ->+ let sdc = envSdc e+ in e { envSdc = sdc+ { extraParams = extraParams sdc ++ dyns+ , extraCons = extraCons sdc ++ [constr]+ }+ , envPhase = NestedPhase+ }++ -- | Generate the dispatch clauses for a resource tree node, plus the+ -- @['String']@ of parents that need a nested-dispatch instance generated.+ -- Names are reported for every parent here, but only the top-level call+ -- keeps them; the parent arm drops its children's names (the deeper+ -- instances are generated by a separate recursion in+ -- 'mkNestedDispatchInstanceWith').+ go :: ResourceTree a -> DispatchM ([String], [Clause])+ go (ResourceParent name _check _attrs pieces children) = do+ -- Delegate to the child's nested-dispatch instance when one already+ -- exists (the configured class: YesodDispatchNested for top-level,+ -- YesodSubDispatchNested for subsites). This is what makes splitting+ -- a parent's nested routes across modules work. When no such instance+ -- exists — the ordinary single-module case — the check fails and we+ -- inline the children below, so this stays backwards compatible.+ -- 'nestedInstanceExists' is the shared probe: it resolves the name once+ -- and saturates by the child's own reified arity, so it never aborts+ -- the splice (see its haddock).+ instanceExists <-+ liftQ (nestedInstanceExists (nestedTargetClass mdsNestedTarget) =<< resolveRouteCon name)++ -- Delegate to the nested-dispatch instance either when one already+ -- exists (cross-module split) or when the caller will generate it in+ -- this same splice ('mdsSameSpliceNestedInstances'). In the latter case+ -- the instance does not exist at probe time, but GHC resolves it after+ -- the whole declaration group is spliced — so we avoid inlining every+ -- nested leaf's dispatch logic here only to re-emit it in the parent's+ -- 'YesodDispatchNested' instance. We still report the name below so that+ -- instance actually gets generated.+ let delegate =+ instanceExists || mdsSameSpliceNestedInstances == GeneratesNestedInstances++ (pats, dyns) <- liftQ $ handlePiecesM pieces+ restName <- liftQ $ newName "_rest"+ helperName <- liftQ $ newName ("helper" ++ name)+ let helperCall = VarE helperName `AppE` VarE restName+ constr = applyConPieces name dyns++ helperClauses <-+ if delegate+ then do+ expr <- delegateToNestedInstance name dyns+ pure [Clause [VarP restName] (NormalB expr) []]+ else do+ -- Inline dispatch: descend into the children with the+ -- extended scope and 'Just' wrapping; their reported names+ -- are dropped (only top-level parents matter).+ childClauses <-+ concatMap snd <$> withChildScope dyns constr (mapM go children)+ pure $ childClauses ++ [Clause [WildP] (NormalB (ConE 'Nothing)) []]++ body <- parentBody helperCall++ -- Report this parent as needing a nested-dispatch instance whenever one+ -- does not already exist — both when we inlined and when we delegated to+ -- a yet-to-be-generated same-splice instance+ -- ('mdsSameSpliceNestedInstances').+ -- Whether the caller acts on this (i.e. actually generates the instance)+ -- is the caller's decision — see 'mkDispatchInstance'.+ pure+ ( [name | not instanceExists]+ , [ Clause+ [mkPathPat (EndRest restName) pats]+ body+ [FunD helperName helperClauses] ]+ )++ go (ResourceLeaf (Resource name pieces dispatch _ _check)) = do+ (pats, dyns) <- liftQ $ handlePiecesM pieces+ (chooseMethod, finalPat) <- handleDispatch name dispatch dyns+ wrap <- askWrap+ let clauseBody = NormalB (wrap chooseMethod)+ pure ([], [Clause [mkPathPat finalPat pats] clauseBody []])++ -- | Delegate body for a parent that already has a nested-dispatch instance:+ -- call the configured nested-dispatch function, passing this route's+ -- dynamics — plus the accumulated parent dynamics from the environment — as+ -- ParentArgs.+ delegateToNestedInstance :: String -> [Exp] -> DispatchM Exp+ delegateToNestedInstance name dyns = do+ sdc <- asks envSdc+ let thisRouteParentArgs = extraParams sdc ++ dyns+ liftQ (nestedDispatchCall (nestedTargetFn mdsNestedTarget) name dyns tyargs sdc thisRouteParentArgs)++ -- | The body of a parent dispatch clause. @helperCall@ runs the parent's+ -- inline helper on the remaining path; per the fallthrough flag we either+ -- fall through on a 'Nothing' (pattern guard) or commit to a 404. The+ -- matched result is run through the current phase's wrapper.+ parentBody :: Exp -> DispatchM Body+ parentBody helperCall = do+ wrap <- askWrap+ if mdsNestedRouteFallthrough+ then liftQ $ mkGuardedBody helperCall (\match' -> pure (wrap (VarE match')))+ else do+ sdc <- asks envSdc+ liftQ $ do+ matchName <- newName "match"+ baseNotFoundExp <-+ [| $(mdsRunHandler) $(mds404) $(pure (envExp sdc)) Nothing $(pure (reqExp sdc)) |]+ pure $ NormalB $ CaseE helperCall+ [ Match (conPCompat 'Just [VarP matchName]) (NormalB (wrap (VarE matchName))) []+ , Match (conPCompat 'Nothing []) (NormalB (wrap baseNotFoundExp)) []+ ]++ -- | Build the chosen-method expression and final path tail for a leaf,+ -- reading the dispatch context ('extraParams' / 'extraCons' / 'envExp' /+ -- 'reqExp') from the environment.+ handleDispatch :: String -> Dispatch a -> [Exp] -> DispatchM (Exp, PathTail)+ handleDispatch name dispatch' dyns = do+ SDC {..} <- asks envSdc+ liftQ $ case dispatch' of+ Methods multi methods -> do+ (finalPat, mfinalE) <-+ case multi of+ Nothing -> return (EndExact, Nothing)+ Just _ -> do+ multiName <- newName "multi"+ return (EndMulti multiName, Just $ VarE multiName)++ let dynsMulti =+ case mfinalE of+ Nothing -> dyns+ Just e -> dyns ++ [e]+ thisRoute = applyConPieces name dynsMulti+ fullRoute = foldr AppE thisRoute extraCons+ jroute = ConE 'Just `AppE` fullRoute+ allDyns = extraParams ++ dynsMulti+ mkRunExp mmethod = do+ runHandlerE <- mdsRunHandler+ handlerE' <- mdsGetHandler mmethod name+ handlerE <- mdsUnwrapper $ foldl' AppE handlerE' allDyns+ return $ runHandlerE+ `AppE` handlerE+ `AppE` envExp+ `AppE` jroute+ `AppE` reqExp++ func <-+ case methods of+ [] -> mkRunExp Nothing+ _ -> do+ getMethod <- mdsMethod+ let methodE = getMethod `AppE` reqExp+ matches <- forM methods $ \method -> do+ exp <- mkRunExp (Just method)+ return $ Match (LitP $ StringL method) (NormalB exp) []+ match405 <- do+ runHandlerE <- mdsRunHandler+ handlerE <- mds405+ let exp = runHandlerE+ `AppE` handlerE+ `AppE` envExp+ `AppE` jroute+ `AppE` reqExp+ return $ Match WildP (NormalB exp) []+ return $ CaseE methodE $ matches ++ [match405]++ return (func, finalPat)++ Subsite _ getSub -> do+ restPath <- newName "restPath"+ let allDyns = extraParams ++ dyns+ sub2 <- mkLambda "sub" $ \sub ->+ pure $ foldl' (\a b -> a `AppE` b) (VarE (mkName getSub) `AppE` VarE sub) allDyns+ routeBuilder <- mkLambda "sroute" $ \sroute ->+ pure $ let thisRoute = applyConPieces name dyns+ in foldr AppE (AppE thisRoute $ VarE sroute) extraCons+ exp <-+ [| $(mdsSubDispatcher)+ $(mdsRunHandler)+ $(pure sub2)+ $(pure routeBuilder)+ $(pure envExp)+ ($(mdsSetPathInfo) $(varE restPath) $(pure reqExp)) |]+ return (exp, EndRest restPath)++ -- The dispatch helper produced by 'mkDispatchClause' is always a terminal+ -- authority (the top-level / subsite entry point), so a final miss commits+ -- to a 404 here. Inline nested helpers built by 'go' under 'withChildScope'+ -- bake their own @Nothing@ fallback at their call site instead.+ mkClause404 envE reqE = do+ exp <- [| $(mdsRunHandler) $(mds404) $(pure envE) Nothing $(pure reqE) |]+ return $ Clause [WildP] (NormalB exp) []++-- | This function generates code to call the nested dispatch function+-- (either 'yesodDispatchNested' or 'yesodSubDispatchNested').+nestedDispatchCall+ :: Name+ -- ^ The dispatch function to call (e.g. 'yesodDispatchNested' or 'yesodSubDispatchNested')+ -> String+ -- ^ The name of the nested route (e.g., "FirstFooR")+ -> [Exp]+ -- ^ The dynamic arguments for this route constructor+ -> TyArgs+ -- ^ Type arguments for parameterized routes+ -> SDC+ -- ^ The accumulated 'SDC'.+ -> [Exp]+ -- ^ The parent dynamic bound variables (for passing as ParentArgs).+ -> Q Exp+nestedDispatchCall dispatchFn routeName routeDyns tyargs sdc parentDyns = do+ routeType <- appliedRouteTypeNamed routeName tyargs+ let wrapper = foldr composeE (applyConPieces routeName routeDyns) (extraCons sdc)+ [| $(varE dispatchFn)+ (Proxy :: Proxy $(pure routeType))+ $(pure $ parentArgsExprFromExps parentDyns)+ $(pure wrapper)+ $(pure $ envExp sdc)+ $(pure $ reqExp sdc)+ |]++-- | Given an 'Exp' which should result in a @'Maybe' a@, does:+--+-- @+-- | Just a <- exp = mkRhs a+-- @+--+mkGuardedBody+ :: Exp+ -- ^ The expression to match Just with+ -> (Name -> Q Exp)+ -- ^ The function to take a 'Name' and create the right hand value on+ -- successful match.+ -> Q Body+mkGuardedBody exp mkRhs = do+ matchName <- newName "match"+ result <- mkRhs matchName+ let patGuard =+ PatG [BindS (conPCompat 'Just [VarP matchName]) exp]+ pure $ GuardedB [(patGuard, result)]++defaultGetHandler :: Maybe String -> String -> Q Exp+defaultGetHandler Nothing s = return $ VarE $ mkName $ "handle" ++ s+defaultGetHandler (Just method) s = return $ VarE $ mkName $ map toLower method ++ s++-- | If the generation of @'YesodDispatch'@ instance require finer+-- control of the types, contexts etc. using this combinator. You will+-- hardly need this generality. However, in certain situations, like+-- when writing library/plugin for yesod, this combinator becomes+-- handy.+mkDispatchInstance+ :: RouteOpts+ -> Type+ -- ^ The master site type+ -> Cxt+ -- ^ Context of the instance+ -> TyArgs+ -- ^ type arguments to constructors+ -> (Exp -> Q Exp)+ -- ^ Unwrap handler+ -> [ResourceTree Type]+ -- ^ The resource+ -> DecsQ+mkDispatchInstance routeOpts master cxt tyargs unwrapper res =+ -- Branch on the focus target via the accessor rather than a constructor+ -- pattern, so 'RouteOpts' can stay abstract (its constructor is not+ -- exported). 'Just target' focuses a single nested route for+ -- module-splitting; 'Nothing' generates the full top-level instance.+ case roFocusOnNestedRoute routeOpts of+ Just target ->+ mkNestedDispatchInstance routeOpts target master cxt tyargs unwrapper res+ Nothing ->+ mkTopLevelDispatchInstance routeOpts master cxt tyargs unwrapper res++mkTopLevelDispatchInstance+ :: RouteOpts+ -> Type+ -> Cxt+ -> TyArgs+ -> (Exp -> Q Exp)+ -> [ResourceTree Type]+ -> DecsQ+mkTopLevelDispatchInstance routeOpts master cxt tyargs unwrapper res = do+ let mds =+ mkMDS+ unwrapper+ [|yesodRunner|]+ [|\parentRunner getSub toParent env -> yesodSubDispatch+ YesodSubRunnerEnv+ { ysreParentRunner = parentRunner+ , ysreGetSub = getSub+ , ysreToParentRoute = toParent+ , ysreParentEnv = env+ }+ |]+ -- Under nested discovery this instance also emits a+ -- 'YesodDispatchNested' instance per top-level parent (see+ -- 'childNamesToGenerate'). When it does, the flat @yesodDispatch@ clause+ -- can /delegate/ each parent to that same-splice instance rather than+ -- inlining the whole subtree's dispatch logic (which the nested instance+ -- would then re-emit). Under 'InlineCompat' no nested instances are+ -- generated, so the flat clause must inline as before.+ usesNestedDiscovery =+ case discoveryMode routeOpts tyargs of+ NestedDiscovery -> True+ InlineCompat -> False+ mdsWithNestedDispatch = mds+ { mdsNestedRouteFallthrough = roNestedRouteFallthrough routeOpts+ , mdsSameSpliceNestedInstances =+ if usesNestedDiscovery+ then GeneratesNestedInstances+ else NoSameSpliceNestedInstances+ }+ (childNames, clause') <- mkDispatchClause tyargs mdsWithNestedDispatch res+ let thisDispatch = FunD 'yesodDispatch [clause']+ -- Only generate 'YesodDispatchNested' instances for children when this+ -- site uses nested discovery. A parameterized site that has not opted+ -- in keeps unparameterized subroute datatypes (see 'discoveryMode' in+ -- RenderRoute), so generating nested instances — which would reference+ -- the parameterized form — must be suppressed to stay consistent.+ childNamesToGenerate =+ if usesNestedDiscovery then childNames else []+ childInstances <-+ fmap mconcat $ forM childNamesToGenerate $ \name -> do+ mkNestedDispatchInstance routeOpts name master cxt tyargs unwrapper res+ return (instanceD cxt yDispatch [thisDispatch] : childInstances)+ where+ yDispatch = ConT ''YesodDispatch `AppT` master++-- | Generate the top-level @YesodDispatchNested@ instance (and the+-- @UrlToDispatch@\/@RedirectUrl@ instances for fragments with no dynamic parent+-- pieces) for a nested route target. A thin wrapper over+-- 'mkNestedDispatchInstanceWith' with the top-level config and the master site.+--+-- @since 1.7.0.0+mkNestedDispatchInstance+ :: RouteOpts+ -> String+ -> Type+ -> Cxt+ -> TyArgs -- ^ tyargs+ -> (Exp -> Q Exp)+ -> [ResourceTree Type]+ -> Q [Dec]+mkNestedDispatchInstance routeOpts target master cxt tyargs unwrapper res =+ mkNestedDispatchInstanceWith TopLevelNested (Just master)+ routeOpts target cxt tyargs unwrapper res++-- | The shared body of the top-level ('mkNestedDispatchInstance') and subsite+-- (@mkNestedSubDispatchInstance@, in "Yesod.Core.Internal.TH") nested-dispatch+-- instance generators. Both+-- find the target, build the parent-dynamics pattern, generate the dispatch+-- clauses via 'genNestedDispatchClauses', emit one+-- 'nestedTargetClass'\/'nestedTargetFn' instance, and recurse into nested+-- children. They differ only in the 'NestedTarget' (which selects the+-- dispatch function\/class, runner, and subsite-env construction) and whether a+-- master site is in play: a @'Just' master@ marks the top-level case and additionally emits+-- the @UrlToDispatch@\/@RedirectUrl@ instances, which the subsite case+-- ('Nothing') never produces.+mkNestedDispatchInstanceWith+ :: NestedTarget+ -> Maybe Type -- ^ @Just master@ ⇒ top-level (also emit UrlToDispatch\/RedirectUrl); @Nothing@ ⇒ subsite+ -> RouteOpts+ -> String+ -> Cxt+ -> TyArgs+ -> (Exp -> Q Exp)+ -> [ResourceTree Type]+ -> Q [Dec]+mkNestedDispatchInstanceWith nestedTarget mmaster routeOpts target cxt tyargs unwrapper res = do+ -- Resolve the target subtree from the root exactly once. The recursion into+ -- nested children below threads the already-resolved @(prePieces, subres)@+ -- straight through ('go'), rather than re-passing the full root and walking+ -- it from scratch for every descendant (which was O(nodes × depth)).+ case findNestedRoute target res of+ Nothing ->+ fail $ "Target '" ++ target ++ "' was not found in resources."+ Just (prePieces, subres) ->+ go target prePieces subres+ where+ go curTarget prePieces subres = do+ -- The parent's static depth and how many dynamic pieces it consumes (only+ -- the count matters below, for the parent-dynamics pattern and the+ -- no-dynamics UrlToDispatch case).+ let parentDepth = length prePieces+ preDyns = [() | Dynamic _ <- prePieces]+ targetT = applyTyArgs (ConT (mkName curTarget)) tyargs++ -- UrlToDispatch/RedirectUrl is a top-level-only convenience and only+ -- possible when ParentArgs ~ () (no dynamic parent pieces).+ urlToDispatchInstances <- case (mmaster, preDyns) of+ (Just master, []) ->+ mkUrlToDispatchRedirectInstances cxt curTarget targetT master+ _ ->+ pure []++ -- Generate the parent dynamic argument variables once, and derive the+ -- binding pattern from them. The clause-generator is handed the @['Name']@+ -- directly (it used to re-decode them out of the pattern).+ parentDynVars <- forM preDyns $ \_ -> newName "parentDyn"+ let parentDynsP = parentArgsPat parentDynVars++ -- Generate names for the instance parameters+ toParentN <- newName "toParentRoute"+ envN <- newName "env"+ reqN <- newName "req"++ -- Generate dispatch clauses for each child resource+ clauses <- genNestedDispatchClauses+ nestedTarget+ routeOpts+ parentDynVars+ (VarE toParentN)+ (VarE envN)+ (VarE reqN)+ unwrapper+ tyargs+ subres++ dropExp <- [| drop parentDepth (W.pathInfo $(varE reqN)) |]+ let dispatchNestedT = ConT (nestedTargetClass nestedTarget) `AppT` targetT+ thisDispatch = FunD (nestedTargetFn nestedTarget)+ [Clause+ [WildP, parentDynsP, VarP toParentN, VarP envN, VarP reqN]+ (NormalB $ CaseE dropExp clauses)+ []+ ]++ childInstances <-+ fmap mconcat $ forM subres $ \childRes -> do+ case childRes of+ ResourceParent name _ _ childPieces grandchildren -> do+ rc <- resolveRouteCon name+ instanceExists <- nestedInstanceExists (nestedTargetClass nestedTarget) rc+ if instanceExists+ then pure []+ else do+ -- Run the same arity guard the top-level+ -- 'mkYesodSubDispatchInstance' applies, so a+ -- 2nd-level-or-deeper nested datatype whose+ -- parameter count doesn't match the (sub)site's+ -- type arguments fails with the actionable message+ -- rather than a cryptic kind error from generated+ -- code. A no-op for the monomorphic (0-arity) case.+ assertNestedSubArity+ (nestedTargetCallSite nestedTarget)+ (SubsiteName curTarget)+ (SubsiteArity (tyArgsArity tyargs))+ rc+ -- Recurse on the already-resolved subtree: this+ -- child's accumulated prefix is the parent's plus+ -- this child's own pieces.+ go name (prePieces <> childPieces) grandchildren+ _ -> pure []++ return+ ( instanceD cxt dispatchNestedT+ [ thisDispatch+ ]+ : childInstances <> urlToDispatchInstances+ )++-- | The top-level-only @UrlToDispatch@ + @RedirectUrl@ instances for a nested+-- route fragment whose @ParentArgs ~ ()@ (no dynamic parent pieces), letting+-- the fragment's constructor be used directly in @setUrl@\/@redirect@ without+-- the @WithParentArgs@ wrapper. Subsite nested dispatch emits none of these.+mkUrlToDispatchRedirectInstances+ :: Cxt+ -> String -- ^ target route name (for the in-scope ToParentRoute check)+ -> Type -- ^ the applied target route type+ -> Type -- ^ the master site type+ -> Q [Dec]+mkUrlToDispatchRedirectInstances cxt target targetT master = do+ urlToDispatchT <- [t| UrlToDispatch $(pure targetT) $(pure master) |]+ urlToDispatchFn <- [e| toWaiAppYreNested (Proxy :: Proxy $(pure targetT)) () |]+ mYesodConstraint <- do+ hasYesodInstance <- isInstance ''Yesod [master]+ if hasYesodInstance+ then pure []+ else do+ yesodContext <- [t| Yesod $(pure master) |]+ pure [yesodContext]+ mToParentRouteConstraint <- do+ mtypeName <- lookupTypeName target+ parentRouteCxt <- [t| ToParentRoute $(pure targetT) |]+ case mtypeName of+ Nothing -> do+ -- must be generating it still. assume we don't+ -- have the instance in scope.+ pure [parentRouteCxt]+ Just _ -> do+ -- type is around, let's make sure it's not+ -- redundant?+ hasToParentRouteInstance <- isInstance ''ToParentRoute [targetT]+ if hasToParentRouteInstance+ then pure []+ else do+ pure [parentRouteCxt]+ redirectT <- [t| RedirectUrl $(pure master) $(pure targetT) |]+ redirectUrlFn <- [e| toTextUrl . WithParentArgs () |]+ pure+ [ instanceD (cxt <> mYesodConstraint <> mToParentRouteConstraint) urlToDispatchT+ [ FunD 'urlToDispatch+ [ Clause [ WildP ] (NormalB urlToDispatchFn) []+ ]+ ]+ -- OVERLAPPABLE so a strictly more specific hand-written instance (e.g.+ -- one carrying type variables, as for a parameterized site) takes+ -- precedence over this generated convenience instance. Note that a+ -- hand-written instance with an identical fully-concrete head still+ -- collides as a duplicate, regardless of this pragma.+ , InstanceD (Just Overlappable) (cxt <> mToParentRouteConstraint) redirectT+ [ FunD 'toTextUrl+ [ Clause [ ] (NormalB redirectUrlFn) [] ]+ ]+ ]+++-- | Generate dispatch clauses for nested dispatch instances.+-- Parameterized by 'NestedTarget' to support both+-- 'YesodDispatchNested' (top-level) and 'YesodSubDispatchNested' (subsite).+genNestedDispatchClauses+ :: NestedTarget+ -> RouteOpts+ -> [Name] -- ^ parent dynamic arg variables+ -> Exp -- ^ toParentRoute expression+ -> Exp -- ^ yre expression (YesodRunnerEnv or YesodSubRunnerEnv)+ -> Exp -- ^ req expression+ -> (Exp -> Q Exp) -- ^ unwrapper+ -> TyArgs -- ^ type arguments for parameterized routes+ -> [ResourceTree Type]+ -> Q [Match]+genNestedDispatchClauses nestedTarget routeOpts parentDynVars toParentE yreE reqE unwrapper tyargs resources = do+ resourceClauses <- traverse genClauseForResource resources++ -- Terminal-miss clause: a nested dispatch instance ALWAYS returns 'Nothing'+ -- when no clause matches the remaining path, regardless of this module's+ -- fallthrough flag. 'yesodDispatchNested'/'yesodSubDispatchNested' are+ -- documented to return 'Nothing' on a miss so that the *caller* (the+ -- delegating parent clause, or the top-level/subsite terminal authority)+ -- decides whether the miss commits to a 404 or falls through to a sibling.+ -- Baking a 404 in here when fallthrough is disabled would let a split-out+ -- child silently override a fallthrough-wanting parent compiled in another+ -- module. The commit semantics of @fallthrough = False@ instead live at the+ -- delegating clauses (the 'ResourceParent' arm below and the inline parent+ -- body in 'mkDispatchClause') and at the terminal authorities.+ let fallbackClause = Match WildP (NormalB (ConE 'Nothing)) []++ return $ concat resourceClauses ++ [fallbackClause]+ where+ genClauseForResource :: ResourceTree Type -> Q [Match]+ genClauseForResource (ResourceLeaf (Resource name pieces dispatch _ _check)) = do+ (pats, dynVars) <- handlePiecesNames pieces+ let routeCon = applyConPieces name (map VarE dynVars)+ allDynVars = parentDynVars ++ dynVars++ case dispatch of+ Methods mmulti methods -> do+ -- Mirror the inline path (see 'handleDispatch' in+ -- 'mkDispatchClause'): a trailing multipiece binds a fresh+ -- @multi@ via 'EndMulti', and that value must be appended both+ -- to the route constructor's arguments and to the handler's+ -- arguments. Hardcoding 'EndExact' here would 404 any non-empty+ -- tail and build the constructor one argument short.+ (finalPat, mMultiE) <- case mmulti of+ Nothing -> pure (EndExact, Nothing)+ Just _ -> do+ multiName <- newName "multi"+ pure (EndMulti multiName, Just (VarE multiName))+ let dynExpsMulti = case mMultiE of+ Nothing -> map VarE dynVars+ Just e -> map VarE dynVars ++ [e]+ routeExp = toParentE `AppE` applyConPieces name dynExpsMulti+ allDynExps = map VarE parentDynVars ++ dynExpsMulti+ handlerExp <- genHandlerCase name methods allDynExps+ body <-+ [| Just ($(varE (nestedTargetRunner nestedTarget))+ $(pure handlerExp)+ $(pure yreE)+ (Just $(pure routeExp))+ $(pure reqE)) |]+ return [Match (mkPathPat finalPat pats) (NormalB body) []]++ Subsite _ getSub -> do+ restPath <- newName "restPath"+ sub2 <- mkLambda "sub" $ \sub ->+ pure $ foldl' AppE (VarE (mkName getSub) `AppE` VarE sub) (map VarE allDynVars)+ routeLam <- mkLambda "sroute" $ \srouteN ->+ [e| $(pure toParentE) ( $(pure routeCon) $(varE srouteN)) |]+ let reqExp' = RecUpdE reqE [('W.pathInfo, VarE restPath)]+ body <- case nestedTarget of+ TopLevelNested ->+ -- Top-level: construct YesodSubRunnerEnv directly+ [| Just (yesodSubDispatch+ YesodSubRunnerEnv+ { ysreParentRunner = yesodRunner+ , ysreGetSub = $(pure sub2)+ , ysreToParentRoute = $(pure routeLam)+ , ysreParentEnv = $(pure yreE)+ }+ $(pure reqExp')) |]+ SubsiteNested ->+ -- Subsite: compose through the outer YesodSubRunnerEnv+ [| Just (yesodSubDispatch+ YesodSubRunnerEnv+ { ysreParentRunner = ysreParentRunner $(pure yreE)+ , ysreGetSub = $(pure sub2) . ysreGetSub $(pure yreE)+ , ysreToParentRoute = ysreToParentRoute $(pure yreE) . $(pure routeLam)+ , ysreParentEnv = ysreParentEnv $(pure yreE)+ }+ $(pure reqExp')) |]+ return [Match (mkPathPat (EndRest restPath) pats) (NormalB body) []]++ genClauseForResource (ResourceParent name _check _attrs pieces _children) = do+ (pats, dynVars) <- handlePiecesNames pieces++ -- Build the parent args tuple for the nested call+ let allDynVars = parentDynVars ++ dynVars+ parentArgsExp = parentArgsExpr allDynVars+ routeConWrapper = applyConPieces name (map VarE dynVars)++ routeType <- appliedRouteTypeNamed name tyargs++ resultName <- newName "k"++ nestedCall <-+ [| $(varE (nestedTargetFn nestedTarget))+ (Proxy :: Proxy $(pure routeType))+ $(pure parentArgsExp)+ ($(pure toParentE) . $(pure routeConWrapper))+ $(pure yreE)+ $(pure reqE) |]++ if roNestedRouteFallthrough routeOpts+ then do+ -- Fallthrough enabled: pattern-guard on a 'Just' result, so a+ -- 'Nothing' from the delegated child lets dispatch fall through+ -- to sibling clauses (and ultimately the 'Nothing' fallback).+ let patGuard = BindS (conPCompat 'Just [VarP resultName]) nestedCall+ guardedBody = GuardedB [(PatG [patGuard], ConE 'Just `AppE` VarE resultName)]+ return [Match (mkPathPat EndWild pats) guardedBody []]+ else do+ -- Fallthrough disabled: once this parent's path prefix matches we+ -- commit to its subtree. If the delegated child dispatch returns+ -- 'Nothing', convert it to a 404 handler call rather than letting+ -- the 'Nothing' propagate (which, under an outer fallthrough+ -- caller, would let it fall through to siblings). This mirrors the+ -- inline no-fallthrough path's @Nothing -> Just 404@ shape.+ committedBody <-+ [| Just (fromMaybe+ ($(varE (nestedTargetRunner nestedTarget))+ (void notFound) $(pure yreE) Nothing $(pure reqE))+ $(pure nestedCall)) |]+ return [Match (mkPathPat EndWild pats) (NormalB committedBody) []]++ genHandlerCase :: String -> [String] -> [Exp] -> Q Exp+ genHandlerCase name methods allDynExps = do+ let handlerExpFor mmethod = do+ handlerName <- defaultGetHandler mmethod name+ unwrapper $ foldl' AppE handlerName allDynExps++ if null methods+ then do+ -- No specific methods, just call handler+ handlerE <- handlerExpFor Nothing+ [| fmap toTypedContent $(pure handlerE) |]+ else do+ -- Generate method case+ -- Wrap each handler with fmap toTypedContent so all branches have the same type+ methodMatches <- forM methods $ \method -> do+ handlerE <- handlerExpFor (Just method)+ wrappedHandler <- [| fmap toTypedContent $(pure handlerE) |]+ return $ Match (LitP $ StringL method) (NormalB wrappedHandler) []++ badMethodHandler <- unwrapper (VarE 'badMethod)+ let badMethodMatch = Match WildP (NormalB badMethodHandler) []+ scrutinee <- [| W.requestMethod $(pure reqE) |]+ return $ CaseE scrutinee (methodMatches ++ [badMethodMatch])++mkYesodSubDispatch :: [ResourceTree a] -> Q Exp+mkYesodSubDispatch = mkYesodSubDispatchWith defaultOpts++-- | Like 'mkYesodSubDispatch', but threads a 'RouteOpts' into the generated+-- @yesodSubDispatch@ body. The only option that affects the body is+-- 'roNestedRouteFallthrough', which controls whether a subsite's top-level+-- parent clause falls through to a later sibling on an inner miss (mirroring+-- 'mkTopLevelDispatchInstance'). 'mkYesodSubDispatch' keeps the opts-less+-- signature for backwards compatibility.+--+-- This standalone entry point never delegates a parent's dispatch to a+-- same-splice 'YesodSubDispatchNested' instance: when called on its own there+-- are no such instances being generated alongside it (delegating would emit a+-- reference to a missing instance). 'mkYesodSubDispatchInstanceOpts', which+-- /does/ generate the matching nested instances in the same splice, uses+-- 'mkYesodSubDispatchWithDelegate' to opt into delegation.+--+-- @since 1.7.0.0+mkYesodSubDispatchWith :: RouteOpts -> [ResourceTree a] -> Q Exp+mkYesodSubDispatchWith = mkYesodSubDispatchWithDelegate NoSameSpliceNestedInstances++-- | The body-generation core shared by the standalone 'mkYesodSubDispatchWith'+-- and the instance-generating 'mkYesodSubDispatchInstanceOpts'. The+-- 'SameSpliceNestedInstances' argument says whether the caller emits the+-- per-parent 'YesodSubDispatchNested' instances in the same splice — see that+-- type for what each choice means for the generated body.+--+-- @since 1.7.0.0+mkYesodSubDispatchWithDelegate+ :: SameSpliceNestedInstances -> RouteOpts -> [ResourceTree a] -> Q Exp+mkYesodSubDispatchWithDelegate sameSplice routeOpts res = do+ let mds = (mkMDS+ return+ [|subHelper|]+ [|subTopDispatch|])+ { mdsNestedTarget = SubsiteNested+ , mdsNestedRouteFallthrough = roNestedRouteFallthrough routeOpts+ , mdsSameSpliceNestedInstances = sameSplice+ }+ (_childNames, clause') <-+ mkDispatchClause+ NoTyArgs+ mds+ res+ inner <- newName "inner"+ let innerFun = FunD inner [clause']+ helper <- newName "helper"+ let fun = FunD helper+ [ Clause+ []+ (NormalB $ VarE inner)+ [innerFun]+ ]+ return $ LetE [fun] (VarE helper)+++subTopDispatch ::+ (YesodSubDispatch sub master) =>+ (forall content. ToTypedContent content =>+ SubHandlerFor child master content ->+ YesodSubRunnerEnv child master ->+ Maybe (Route child) ->+ W.Application+ ) ->+ (mid -> sub) ->+ (Route sub -> Route mid) ->+ YesodSubRunnerEnv mid master ->+ W.Application+subTopDispatch _ getSub toParent env =+ yesodSubDispatch+ ( YesodSubRunnerEnv+ { ysreParentRunner = ysreParentRunner env+ , ysreGetSub = getSub . ysreGetSub env+ , ysreToParentRoute = ysreToParentRoute env . toParent+ , ysreParentEnv = ysreParentEnv env+ }+ )++mkMDS :: (Exp -> Q Exp) -> Q Exp -> Q Exp -> MkDispatchSettings a site b+mkMDS unwrapper runHandlerE subDispatcher = MkDispatchSettings+ { mdsRunHandler = runHandlerE+ , mdsSubDispatcher = subDispatcher+ , mdsGetPathInfo = [|W.pathInfo|]+ , mdsSetPathInfo = [|\p r -> r { W.pathInfo = p }|]+ , mdsMethod = [|W.requestMethod|]+ , mds404 = [|void notFound|]+ , mds405 = [|void badMethod|]+ , mdsGetHandler = defaultGetHandler+ , mdsUnwrapper = unwrapper+ , mdsNestedRouteFallthrough = False+ , mdsNestedTarget = TopLevelNested+ , mdsSameSpliceNestedInstances = NoSameSpliceNestedInstances+ }++-- | Parse the foundation-type string given to @mkYesod@ into its components:+-- the type-constructor name, its type arguments, and the @=>@ class-context+-- groups (each a class name applied to type arguments). Returns 'Left' with a+-- parse-error message on malformed input.+--+-- @since 1.7.0.0+parseYesodName :: String -> Either String (String, [String], [[String]])+parseYesodName name = do+ either (Left . show) Right $ parse parseName "" name+ where+ parseName = do+ cxt <- option [] parseContext+ name' <- parseWord+ args <- many parseWord+ spaces+ eof+ return ( name', args, cxt)++ parseWord = do+ spaces+ many1 alphaNum++ parseContext = try $ do+ cxts <- parseParen parseContexts+ spaces+ _ <- string "=>"+ return cxts++ parseParen p = do+ spaces+ _ <- char '('+ r <- p+ spaces+ _ <- char ')'+ return r++ parseContexts =+ sepBy1 (many1 parseWord) (spaces >> char ',' >> return ())
+ src/Yesod/Routes/TH/Internal.hs view
@@ -0,0 +1,412 @@++{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TemplateHaskell #-}++module Yesod.Routes.TH.Internal where++import Prelude hiding (exp)+import Data.List (foldl')+import Data.Maybe (mapMaybe)+-- newName is hidden so the only one in scope is the 'Quote' class method+-- (re-exported by th-compat); with template-haskell < 2.17 the monomorphic+-- 'Language.Haskell.TH.Syntax.newName' is a distinct, ambiguous name.+import Language.Haskell.TH.Syntax hiding (newName)+import Language.Haskell.TH.Syntax.Compat (Quote(..))+import Web.PathPieces (fromPathPiece, fromPathMultiPiece)+import Yesod.Routes.TH.Types++conPCompat :: Name -> [Pat] -> Pat+conPCompat n pats = ConP n+#if MIN_VERSION_template_haskell(2,18,0)+ []+#endif+ pats++instanceD :: Cxt -> Type -> [Dec] -> Dec+instanceD = InstanceD Nothing++mkTupE :: [Exp] -> Exp+mkTupE =+ TupE+#if MIN_VERSION_template_haskell(2,16,0)+ . fmap Just+#endif++-- | The expression form of a route's accumulated @ParentArgs@: @()@ when there+-- are no dynamic pieces, the lone expression when there is one, and a tuple of+-- them otherwise. This is the single spelling of the unit\/single\/tuple idiom+-- that recurred across the dispatch and render-route clause builders.+parentArgsExprFromExps :: [Exp] -> Exp+parentArgsExprFromExps [] = ConE '()+parentArgsExprFromExps [e] = e+parentArgsExprFromExps es = mkTupE es++-- | 'parentArgsExprFromExps' for the common case where the arguments are plain+-- variable names.+parentArgsExpr :: [Name] -> Exp+parentArgsExpr = parentArgsExprFromExps . map VarE++-- | The /pattern/ form of a route's accumulated @ParentArgs@, for binding the+-- incoming parent arguments: @_@ when there are none (the @ParentArgs@ is @()@+-- and ignored), the lone variable when there is one, and a tuple otherwise.+parentArgsPat :: [Name] -> Pat+parentArgsPat [] = WildP+parentArgsPat [a] = VarP a+parentArgsPat as = TupP (map VarP as)++-- | The /type/ form of a route's accumulated @ParentArgs@: the unit type when+-- there are no dynamic pieces, the lone type when there is one, and a tuple+-- type otherwise.+parentArgsType :: [Type] -> Type+parentArgsType [] = ConT ''()+parentArgsType [t] = t+parentArgsType ts = foldl' AppT (TupleT (length ts)) ts++-- | The number of type parameters a type constructor declares (0 for a type+-- that isn't a data\/newtype\/type-synonym, or that we can't reify).+typeArity :: Name -> Q Int+typeArity typeName = do+ info <- reify typeName+ pure $ case info of+ TyConI (DataD _ _ vs _ _ _) -> length vs+ TyConI (NewtypeD _ _ vs _ _ _) -> length vs+ TyConI (TySynD _ vs _) -> length vs+ _ -> 0++-- | Look up a type by 'Name' and return it fully applied with fresh+-- type variables. This is needed because nested route datatypes may+-- have type parameters (e.g., @NestedR subsite@), and TH functions+-- like 'isInstance' require fully-applied (kind 'Type') heads.+--+-- The application is to the datatype's /own/ reified 'typeArity', so the head+-- is saturated exactly — neither under- nor over-applied — and is therefore+-- well-kinded by construction. This holds for higher-kinded parameters too:+-- a parameter declared @(f :: Type -> Type)@ is filled with a bare,+-- unannotated @'VarT' a@, and GHC infers @a :: Type -> Type@ from the+-- datatype's declared kind, so we don't need to reconstruct each parameter's+-- kind ourselves. (Saturating by some /other/ arity — e.g. a caller's+-- 'TyArgs' count — is what historically built ill-kinded heads and made+-- 'isInstance' throw; see 'nestedInstanceExists'.)+fullyApplyType :: Name -> Q Type+fullyApplyType typeName = do+ arity <- typeArity typeName+ vars <- mapM (\i -> VarT <$> newName ("a" ++ show i)) [1..arity]+ pure $ foldl' AppT (ConT typeName) vars++-- | A route datatype name paired with the result of resolving it in the+-- current splice environment. 'rcResolved' is 'Just' when the datatype is+-- already in scope (defined in an earlier splice or a separately compiled+-- module) and 'Nothing' when it is not yet — typically because it is being+-- generated in the same splice group.+--+-- The point is to resolve a route name /once/, at the boundary, and carry the+-- answer in a value rather than re-running 'lookupTypeName' at each use site+-- and independently re-deciding what an unresolved name means. The two+-- consumers that matter — \"does its nested instance already exist?\"+-- ('nestedInstanceExists') and \"what type head do I splice?\"+-- ('appliedRouteTypeCon') — then share a single resolution.+--+-- @since 1.7.0.0+data RouteCon = RouteCon+ { rcName :: String -- ^ the route datatype name as written+ , rcResolved :: Maybe Name -- ^ the resolved 'Name', if in scope+ }++-- | Resolve a route datatype name with 'lookupTypeName'. This is the single+-- boundary at which a route name is looked up; downstream code branches on the+-- resulting 'RouteCon' instead of resolving again.+--+-- @since 1.7.0.0+resolveRouteCon :: String -> Q RouteCon+resolveRouteCon name = RouteCon name <$> lookupTypeName name++-- | Does an instance of the given (nested-discovery) class — e.g.+-- @''YesodDispatchNested@, @''RenderRouteNested@, @''ParseRouteNested@,+-- @''RouteAttrsNested@ — already exist for this route datatype? This is the+-- single delegation probe shared by all the generators: when it answers 'True'+-- the parent delegates to the existing instance, and when 'False' it inlines+-- (or generates) the child itself.+--+-- Two rules, applied uniformly so the probe can never abort a splice:+--+-- * An unresolved 'RouteCon' (the datatype is being generated in the same+-- splice) trivially has no instance yet, so the answer is 'False'.+-- * A resolved one is saturated by its /own/ reified arity via+-- 'fullyApplyType' — never by the site's 'TyArgs'. Applying the site's+-- type args to a child that was generated at a different arity (e.g. a+-- kind-'Type' child of a parameterized site) builds an ill-kinded head,+-- and 'isInstance' /throws/ a kind error rather than returning 'False',+-- aborting the splice. Saturating by the datatype's own arity keeps the+-- probed head well-kinded in every case.+--+-- /Can the abstract parameters make 'isInstance' fail?/ No. Once the head is+-- well-kinded (above), 'isInstance' answers by head unification — \"could+-- /any/ visible instance match this head?\" — which is a total query: it+-- returns 'True' or 'False' but cannot crash on an abstract @'VarT' a@. The+-- one theoretical imprecision is /over-matching/: because the argument is an+-- unconstrained variable, an instance written only at a more specific type+-- (e.g. @instance C (R Int)@) would still unify with @C (R a)@ and report+-- 'True'. That is harmless here because our codegen emits the nested-discovery+-- instances uniformly, one per route datatype at a fully abstract parameter+-- (e.g. @instance YesodDispatchNested (R a)@), so \"could match\" and \"does+-- match\" coincide — there is no narrower instance for the probe to confuse a+-- variable with. (Higher-kinded parameters are covered by the same argument;+-- see 'fullyApplyType'.)+--+-- @since 1.7.0.0+nestedInstanceExists :: Name -> RouteCon -> Q Bool+nestedInstanceExists klass rc =+ case rcResolved rc of+ Nothing -> pure False+ Just typeName -> do+ appliedT <- fullyApplyType typeName+ isInstance klass [appliedT]++-- | The fully-applied route 'Type' for a 'RouteCon', as used for an instance+-- head. This is the single place the \"saturate by site 'TyArgs' vs. by the+-- datatype's own reified arity\" choice is made:+--+-- * With explicit 'TyArgs' (a parameterized site) the supplied type+-- arguments are applied to the constructor — the resolved 'Name' if in+-- scope, otherwise the bare 'mkName' (so a datatype being generated in the+-- same splice still gets a head to reference).+-- * With 'NoTyArgs' a resolved datatype is reified and saturated with fresh+-- type variables (needed for e.g. @mkYesodSubDispatch@, which doesn't know+-- the parent's type args); an unresolved one falls back to the bare+-- constructor.+--+-- @since 1.7.0.0+appliedRouteTypeCon :: RouteCon -> TyArgs -> Q Type+appliedRouteTypeCon rc tyargs =+ case (rcResolved rc, tyargs) of+ (Just typeName, NoTyArgs) -> fullyApplyType typeName+ (Just typeName, SomeTyArgs{}) -> pure $ applyTyArgs (ConT typeName) tyargs+ (Nothing, _) -> pure $ applyTyArgs (ConT (mkName (rcName rc))) tyargs++-- | 'appliedRouteTypeCon' starting from the route datatype's name as a+-- 'String': resolve it ('resolveRouteCon') and apply the type arguments. The+-- fallback for an unresolved name (the bare constructor) is why this can't be+-- a plain @'lookupTypeName' >>=@ that requires the name to be in scope.+appliedRouteTypeNamed :: String -> TyArgs -> Q Type+appliedRouteTypeNamed routeName tyargs = do+ rc <- resolveRouteCon routeName+ appliedRouteTypeCon rc tyargs++-- | A subsite's type-constructor name. Distinct from 'RouteName' so the two+-- can't be swapped at a 'checkNestedSubArity' call site without a type error.+--+-- @since 1.7.0.0+newtype SubsiteName = SubsiteName String deriving (Eq, Show)++-- | A nested route datatype's name. See 'SubsiteName'.+--+-- @since 1.7.0.0+newtype RouteName = RouteName String deriving (Eq, Show)++-- | How many type arguments a subsite has. Distinct from 'RouteArity' so the+-- two arities can't be swapped (the equality predicate is symmetric, but the+-- error message is not).+--+-- @since 1.7.0.0+newtype SubsiteArity = SubsiteArity Int deriving (Eq, Show)++-- | The declared type-parameter arity of a nested route datatype. See+-- 'SubsiteArity'.+--+-- @since 1.7.0.0+newtype RouteArity = RouteArity Int deriving (Eq, Show)++-- | A detected mismatch between a subsite's type-argument count and its nested+-- route datatype's declared arity. Carrying the four facts (rather than a+-- pre-rendered 'String') lets the caller decide how to surface it and lets+-- tests assert on structure instead of substring-matching a message.+--+-- @since 1.7.0.0+data ArityMismatch = ArityMismatch+ { amSubsiteName :: SubsiteName+ , amRouteName :: RouteName+ , amSubsiteArity :: SubsiteArity+ , amRouteArity :: RouteArity+ } deriving (Eq, Show)++-- | Validate that a parameterized subsite's nested route datatype carries+-- exactly as many type parameters as the subsite has type arguments: 'Nothing'+-- when the arities match, or a structured 'ArityMismatch' otherwise.+--+-- 'mkYesodSubDispatchInstance' applies the subsite's type arguments to each+-- nested route datatype (e.g. @NestedR subsite@) and uses the result directly+-- as the head of a @YesodSubDispatchNested@ instance, which expects a fully+-- applied (kind 'Type') route. So the arities must match exactly:+--+-- * Too few parameters on the nested datatype (e.g. it was generated by+-- plain 'mkYesodSubData', kind 'Type') over-applies it.+-- * Too many leaves it partially applied (kind @Type -> ...@).+--+-- Either way the result is a kind error reported deep inside generated code.+-- Surfacing it here (the caller passes the 'ArityMismatch' to 'fail') turns+-- it into an actionable, attributable message.+--+-- Pure so it can be unit-tested directly.+--+-- @since 1.7.0.0+checkNestedSubArity+ :: SubsiteName+ -> RouteName+ -> SubsiteArity+ -> RouteArity+ -> Maybe ArityMismatch+checkNestedSubArity subName routeName (SubsiteArity subArgs) (RouteArity nestedArity)+ | subArgs == nestedArity = Nothing+ | otherwise = Just $ ArityMismatch subName routeName (SubsiteArity subArgs) (RouteArity nestedArity)++-- | Which generator is running the arity check. The arity facts are identical+-- either way; this only steers 'arityMismatchMessage' so the failure names the+-- right entry point and calls the parameterized thing by its correct term+-- (a top-level site reached through the nested-dispatch recursion is not a+-- \"subsite\").+--+-- @since 1.7.0.0+data ArityCallSite+ = SubsiteCall -- ^ a subsite, generated by 'mkYesodSubDispatchInstance'+ | TopLevelCall -- ^ a top-level site's nested-dispatch recursion+ deriving (Eq, Show)++-- | Run 'checkNestedSubArity' for a (possibly unresolved) nested route+-- datatype and 'fail' with 'arityMismatchMessage' on a mismatch. A no-op when+-- the datatype is not in scope — an unresolved name has no knowable arity, so+-- the error (if any) surfaces from the generated code instead. This is the+-- single arity-guard shared by 'mkYesodSubDispatchInstance' and the nested+-- dispatch-instance recursion.+--+-- @since 1.7.0.0+assertNestedSubArity :: ArityCallSite -> SubsiteName -> SubsiteArity -> RouteCon -> Q ()+assertNestedSubArity callSite subName subArity rc =+ case rcResolved rc of+ Nothing -> pure ()+ Just tyname -> do+ nestedArity <- typeArity tyname+ maybe (pure ()) (fail . arityMismatchMessage callSite) $+ checkNestedSubArity subName (RouteName (rcName rc)) subArity (RouteArity nestedArity)++-- | The actionable @fail@ message for an 'ArityMismatch', phrased for the+-- 'ArityCallSite' that detected it.+--+-- @since 1.7.0.0+arityMismatchMessage :: ArityCallSite -> ArityMismatch -> String+arityMismatchMessage callSite (ArityMismatch (SubsiteName subName) (RouteName routeName) (SubsiteArity subArgs) (RouteArity nestedArity)) =+ concat+ [ prefix, ": the nested route datatype `", routeName+ , "` has ", show nestedArity, " type parameter(s), but the ", term, " `"+ , subName, "` has ", show subArgs+ , ". The subroute datatypes must carry exactly the ", term, "'s type "+ , "parameter(s) — generate the ", term, "'s routes with "+ , "`mkYesodSubDataOpts (setParameterizedSubroute True defaultOpts) ...` "+ , "so a parameterized ", term, " gets parameterized subroutes."+ ]+ where+ (prefix, term) = case callSite of+ SubsiteCall -> ("mkYesodSubDispatchInstance", "subsite")+ TopLevelCall -> ("mkYesod", "site")++-- | Turn a single route piece into a match pattern, returning the freshly+-- bound 'Name' for dynamic pieces (and 'Nothing' for static ones). The+-- fresh-'Name' supply is the 'Quote' @m@ it runs in: the 'Q' code generators+-- run at 'Q' (hygienic 'newName'), while the pure, deterministic inline-parse+-- path runs at a 'Quote' instance over a monotonic counter (see the test+-- harness). This is the single source of truth for the+-- @ViewP fromPathPiece (Just x)@ dynamic encoding shared by the parse and+-- dispatch clause builders.+handlePieceM :: Quote m => Piece a -> m (Pat, Maybe Name)+handlePieceM (Static str) = pure (LitP $ StringL str, Nothing)+handlePieceM (Dynamic _) = mk <$> newName "dyn"+ where+ mk x = (ViewP (VarE 'fromPathPiece) (conPCompat 'Just [VarP x]), Just x)++-- | The list form of 'handlePieceM' projecting the dynamic binders as 'Name's,+-- for callers that consume the bound variables directly (e.g. nested-dispatch+-- handler arguments).+handlePiecesNames :: Quote m => [Piece a] -> m ([Pat], [Name])+handlePiecesNames =+ fmap (\pps -> (map fst pps, mapMaybe snd pps)) . traverse handlePieceM++-- | The list form of 'handlePieceM' projecting the dynamic binders as 'VarE'+-- expressions, for callers that rebuild a route by applying its constructor to+-- the captured pieces.+handlePiecesM :: Quote m => [Piece a] -> m ([Pat], [Exp])+handlePiecesM =+ fmap (\(pats, names) -> (pats, map VarE names)) . handlePiecesNames++-- | Rebuild a route by applying its constructor (named by 'String', resolved+-- with 'mkName') to the captured dynamic-piece expressions — the companion to+-- 'handlePiecesM', which produces the @['Exp']@. This is the single spelling+-- of the @'foldl'' 'AppE' ('ConE' ('mkName' name))@ route-construction idiom+-- that recurred across the dispatch and parse clause builders.+applyConPieces :: String -> [Exp] -> Exp+applyConPieces name = foldl' AppE (ConE (mkName name))++-- | @composeE f g@ is the expression @f . g@.+composeE :: Exp -> Exp -> Exp+composeE f g = InfixE (Just f) (VarE '(.)) (Just g)++-- | @consE x xs@ is the expression @x : xs@.+consE :: Exp -> Exp -> Exp+consE x xs = InfixE (Just x) (ConE '(:)) (Just xs)++-- | How a path match pattern ends, after its matched piece patterns. Only+-- these four shapes are ever valid as a path tail, so making it a closed type+-- (rather than an arbitrary 'Pat') stops a caller from silently matching the+-- wrong number of segments — e.g. binding a @rest@ where the path was meant to+-- end exactly.+data PathTail+ = EndExact -- ^ @[]@ — the path ends exactly here+ | EndRest Name -- ^ bind the remaining segments to the given variable+ | EndWild -- ^ @_@ — ignore any remaining segments+ | EndMulti Name -- ^ a trailing multipiece: @'fromPathMultiPiece' -> 'Just' n@++-- | The 'Pat' a 'PathTail' denotes (the seed the matched piece patterns are+-- consed onto in 'mkPathPat').+pathTailPat :: PathTail -> Pat+pathTailPat EndExact = conPCompat '[] []+pathTailPat (EndRest n) = VarP n+pathTailPat EndWild = WildP+pathTailPat (EndMulti n) = ViewP (VarE 'fromPathMultiPiece) (conPCompat 'Just [VarP n])++-- | Build a path match pattern: a list pattern of the given piece patterns+-- ending in the given 'PathTail'.+mkPathPat :: PathTail -> [Pat] -> Pat+mkPathPat tl = foldr (\x y -> conPCompat '(:) [x, y]) (pathTailPat tl)++-- | Generate a single-argument lambda expression with a fresh name.+-- Takes a base name hint, generates a unique 'Name', and passes it+-- to a callback that produces the lambda body. Returns the complete+-- 'LamE' expression.+--+-- For multi-argument lambdas, nest calls — @\\x y -> body@ is+-- equivalent to @\\x -> \\y -> body@.+mkLambda :: String -> (Name -> Q Exp) -> Q Exp+mkLambda hint mkBody = do+ n <- newName hint+ bdy <- mkBody n+ pure $ LamE [VarP n] bdy++-- | Given a target 'String', find the 'ResourceParent' in the+-- @['ResourceTree' a]@ corresponding to that target and return it.+-- Also return the @['Piece' a]@ captures that precede it.+findNestedRoute :: String -> [ResourceTree a] -> Maybe ([Piece a], [ResourceTree a])+findNestedRoute _ [] = Nothing+findNestedRoute target (res : ress) =+ case res of+ ResourceLeaf _ ->+ findNestedRoute target ress+ ResourceParent name _overlap _attrs pieces children -> do+ if name == target+ then Just (pieces, children)+ else+ let mresult = findNestedRoute target children+ in+ case mresult of+ Nothing -> do+ findNestedRoute target ress+ Just (typs, childRoute) -> do+ Just (pieces <> typs, childRoute)
src/Yesod/Routes/TH/ParseRoute.hs view
@@ -1,47 +1,334 @@ {-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+ module Yesod.Routes.TH.ParseRoute ( -- ** ParseRoute mkParseRouteInstance+ , mkParseRouteInstanceOpts+ , mkParseRouteInstanceFor+ -- ** Clause construction (monad-polymorphic over 'Quote'; testable purely)+ , buildInlineParseClauses+ , generateParseRouteClause ) where +import qualified Control.Monad.Trans as Trans+import qualified Data.Set as Set+import Control.Monad import Yesod.Routes.TH.Types-import Language.Haskell.TH.Syntax-import Data.Text (Text)+import Language.Haskell.TH (varE, varP)+import Language.Haskell.TH.Syntax hiding (newName)+import Language.Haskell.TH.Syntax.Compat (Quote(..), unsafeQToQuote) import Yesod.Routes.Class-import Yesod.Routes.TH.Dispatch+import Yesod.Routes.TH.RenderRoute+import Control.Monad.State.Strict+import Yesod.Routes.TH.Internal -mkParseRouteInstance :: Cxt -> Type -> [ResourceTree a] -> Q Dec-mkParseRouteInstance cxt typ ress = do- cls <- mkDispatchClause- MkDispatchSettings- { mdsRunHandler = [|\_ _ x _ -> x|]- , mds404 = [|error "mds404"|]- , mds405 = [|error "mds405"|]- , mdsGetPathInfo = [|fst|]- , mdsMethod = [|error "mdsMethod"|]- , mdsGetHandler = \_ _ -> [|error "mdsGetHandler"|]- , mdsSetPathInfo = [|\p (_, q) -> (p, q)|]- , mdsSubDispatcher = [|\_runHandler _getSub toMaster _env -> fmap toMaster . parseRoute|]- , mdsUnwrapper = return- }- (map removeMethods ress)- helper <- newName "helper"- fixer <- [|(\f x -> f () x) :: (() -> ([Text], [(Text, Text)]) -> Maybe (Route a)) -> ([Text], [(Text, Text)]) -> Maybe (Route a)|]- return $ instanceD cxt (ConT ''ParseRoute `AppT` typ)- [ FunD 'parseRoute $ return $ Clause- []- (NormalB $ fixer `AppE` VarE helper)- [FunD helper [cls]]- ]+-- | The trailing @_ -> Nothing@ clause appended to every generated+-- @parseRoute@\/@parseRouteNested@: a path matching none of the preceding+-- clauses fails to parse.+missingRouteClause :: Clause+missingRouteClause = Clause [WildP] (NormalB (ConE 'Nothing)) []++mkParseRouteInstance :: TyArgs -> Cxt -> Type -> [ResourceTree a] -> Q [Dec]+mkParseRouteInstance =+ mkParseRouteInstanceOpts defaultOpts++-- | Generate a ParseRouteNested instance for a specific nested route target.+-- This is a convenience wrapper around mkParseRouteInstanceOpts with setFocusOnNestedRoute.+--+-- @since 1.7.0.0+mkParseRouteInstanceFor :: String -> [ResourceTree a] -> Q [Dec]+mkParseRouteInstanceFor target ress = do+ let opts = setFocusOnNestedRoute target defaultOpts+ targetType = ConT (mkName target)+ mkParseRouteInstanceOpts opts NoTyArgs [] targetType ress++-- | Generate the 'ParseRoute' instance (and, under nested discovery, the+-- accompanying 'ParseRouteNested' instances) for a site, honoring the supplied+-- 'RouteOpts' and the site's 'TyArgs'. The options-driven core that+-- 'mkParseRouteInstanceFor' and the @mkYesod@ entry points build on.+--+-- @since 1.7.0.0+mkParseRouteInstanceOpts :: RouteOpts -> TyArgs -> Cxt -> Type -> [ResourceTree a] -> Q [Dec]+mkParseRouteInstanceOpts routeOpts origTyargs cxt typ unfocusedRess =+ case discoveryMode routeOpts origTyargs of+ -- Backwards-compatible default: generate a single 'ParseRoute'+ -- instance with all nested routes inlined, and no 'ParseRouteNested'+ -- instances.+ InlineCompat -> do+ clausess <- mapM (buildInlineParseClauses id) unfocusedRess+ let allClauses = concat clausess <> [missingRouteClause]+ pure+ [ instanceD cxt (ConT ''ParseRoute `AppT` typ)+ [ FunD 'parseRoute allClauses+ ]+ ]+ NestedDiscovery -> do+ ress <- focusTarget unfocusedRess+ existingInstances <- existingNestedInstances ress+ (clauses, childNames) <- flip runStateT mempty $ traverse (generateParseRouteClause existingInstances routeOpts) ress++ childInstances <- fmap join $ forM (Set.toList childNames) $ \childName ->+ mkParseRouteInstanceOpts routeOpts { roFocusOnNestedRoute = Just childName } origTyargs cxt (targetTypeFor childName) ress++ let allClauses = clauses <> [missingRouteClause]++ let thisInstance =+ case roFocusOnNestedRoute routeOpts of+ Just target ->+ instanceD cxt (ConT ''ParseRouteNested `AppT` targetTypeFor target)+ [ FunD 'parseRouteNested allClauses+ ]+ Nothing ->+ instanceD cxt (ConT ''ParseRoute `AppT` typ)+ [ FunD 'parseRoute allClauses+ ]++ pure $ thisInstance : childInstances where- -- We do this in order to ski the unnecessary method parsing- removeMethods (ResourceLeaf res) = ResourceLeaf $ removeMethodsLeaf res- removeMethods (ResourceParent w x y z) = ResourceParent w x y $ map removeMethods z+ -- The route datatype named by @name@, applied to the site's type arguments.+ targetTypeFor name = applyTyArgs (ConT (mkName name)) origTyargs - removeMethodsLeaf res = res { resourceDispatch = fixDispatch $ resourceDispatch res }+ -- Narrow to the focused subtree's children via the shared lookup, failing+ -- loudly (matching Dispatch/RenderRoute) when the target is missing rather+ -- than silently producing an always-'Nothing' 'ParseRouteNested' instance.+ focusTarget ts =+ case roFocusOnNestedRoute routeOpts of+ Just target ->+ case findNestedRoute target ts of+ Nothing ->+ fail $ "Target '" ++ target ++ "' was not found in resources."+ Just (_prePieces, children) ->+ pure children+ Nothing ->+ pure ts - fixDispatch (Methods x _) = Methods x []- fixDispatch x = x+-- | Probe (in 'Q') which of the given resources' top-level parents already+-- have a 'ParseRouteNested' instance. Pulling these compiler queries+-- ('isInstance', via 'nestedInstanceExists') out of 'generateParseRouteClause'+-- is what lets that generator be monad-polymorphic over 'Quote' — and thus+-- runnable, and testable, at a pure 'Quote' instance.+--+-- @since 1.7.0.0+existingNestedInstances :: [ResourceTree a] -> Q (Set.Set String)+existingNestedInstances ress = do+ flags <- mapM check ress+ pure $ Set.fromList (concat flags)+ where+ check (ResourceParent name _ _ _ _) = do+ rc <- resolveRouteCon name+ has <- nestedInstanceExists ''ParseRouteNested rc+ pure [name | has]+ check ResourceLeaf{} = pure [] -instanceD :: Cxt -> Type -> [Dec] -> Dec-instanceD = InstanceD Nothing+-- | Generate a single @parseRouteNested@-delegating clause for one resource+-- (under nested discovery). It records, into its 'StateT' accumulator, the+-- name of each parent that still needs a 'ParseRouteNested' instance generated.+--+-- The \"does this name already have an instance?\" decision is a compiler query+-- ('isInstance'), which would pin this to 'Q'; it is instead /hoisted out/ and+-- passed in as @existingInstances@, leaving only fresh-name generation as an+-- effect. That makes the generator monad-polymorphic over 'Quote': it runs at+-- 'Q' in production and at a pure 'Quote' instance (a monotonic counter) in+-- tests.+--+-- @since 1.7.0.0+generateParseRouteClause+ :: Quote m+ => Set.Set String+ -- ^ Route names that already have a 'ParseRouteNested' instance (so this+ -- clause should not re-request one). Computed by the caller in 'Q' via+ -- 'nestedInstanceExists'; pass 'mempty' in pure tests.+ -> RouteOpts+ -> ResourceTree a+ -> StateT (Set.Set String) m Clause+generateParseRouteClause existingInstances routeOpts resourceTree =+ case resourceTree of+ ResourceLeaf (Resource name pieces dispatch _ _check) -> do+ (pats, dyns) <- liftQ $ handlePiecesM pieces++ case dispatch of+ Methods multi _ -> do+ (finalTail, dyns') <-+ case multi of+ Nothing ->+ pure (EndExact, dyns)+ Just _ -> do+ multiName <- liftQ $ newName "multi"+ pure (EndMulti multiName, dyns ++ [VarE multiName])++ queryParamsName <- liftQ $ newName "_queryParams"+ let route = applyConPieces name dyns'+ jroute = ConE 'Just `AppE` route+ pathPat = mkPathPat finalTail pats+ pat = TupP [pathPat, VarP queryParamsName]+ pure $ Clause [pat] (NormalB jroute) []++ Subsite _ _ -> do+ restName <- liftQ $ newName "rest"+ queryParamsName <- liftQ $ newName "_queryParams"++ let route = applyConPieces name dyns+ pathPat = mkPathPat (EndRest restName) pats+ pat = TupP [pathPat, VarP queryParamsName]+ tupExp = mkTupE [VarE restName, VarE queryParamsName]+ expr = VarE 'fmap+ `AppE` route+ `AppE` (VarE 'parseRoute `AppE` tupExp)+ pure $ Clause [pat] (NormalB expr) []++ ResourceParent name _check _attrs pieces _children -> do+ recordNameIfNotInstance name++ (pats, dyns) <- liftQ $ handlePiecesM pieces++ let route = applyConPieces name dyns++ restName <- liftQ $ newName "rest"+ queryParamsName <- liftQ $ newName "_queryParams"++ let parseRouteOnRest =+ VarE 'parseRouteNested+ `AppE` mkTupE [VarE restName, VarE queryParamsName]++ body <-+ if roNestedRouteFallthrough routeOpts+ then do+ resultName <- liftQ $ newName "result"+ let stmt = BindS (AsP resultName (conPCompat 'Just [WildP])) parseRouteOnRest+ expr = VarE 'fmap `AppE` route `AppE` VarE resultName+ pure $ GuardedB [(PatG [stmt], expr)]+ else+ pure $ NormalB (VarE 'fmap `AppE` route `AppE` parseRouteOnRest)++ let pat = TupP [mkPathPat (EndRest restName) pats, VarP queryParamsName]+ pure $ Clause [pat] body []++ where+ liftQ :: Monad n => n a -> StateT (Set.Set String) n a+ liftQ = Trans.lift++ -- Record that @name@ still needs a 'ParseRouteNested' instance generated.+ -- The instance-existence decision is hoisted out (into @existingInstances@,+ -- computed in 'Q' by the caller), so this stays pure.+ recordNameIfNotInstance name =+ unless (name `Set.member` existingInstances) $+ modify (Set.insert name)++-- | Backwards-compatible inline @parseRoute@ clause generation. It assembles+-- the @parseRoute@ clauses for the inline path directly as AST, drawing fresh+-- names from a 'Quote' name supply and building tuples\/applications by hand+-- rather than through quotation brackets+-- (brackets are monomorphic 'Q' before template-haskell 2.17; the one+-- exception goes through 'unsafeQToQuote').+--+-- Each call yields one clause per resource tree: a leaf is its own matching+-- clause, while a parent is a single clause that matches its path prefix and+-- then cases the remaining @(path, queryParams)@ over its children (their+-- clauses turned into match alternatives) ending in a @_ -> 'Nothing'@+-- fallback. That fallback is the commit-on-parent-prefix behaviour: once the+-- parent prefix matches, a child miss is a definite 'Nothing' rather than a+-- fall-through to a sibling top-level route.+--+-- It is monad-polymorphic in the name supply via 'Quote': production runs it at+-- 'Q' (so binders are hygienic 'newName's), while tests run it at a pure+-- 'Quote' instance backed by a monotonic counter, where names are deterministic+-- and the '[Clause]' output can be asserted on directly without splicing or+-- 'runQ'. Under the deterministic supply every name is a freshly-bound pattern+-- variable scoped to its clause, the shared counter keeps them distinct within a+-- tree, and it is threaded through the parent\/child recursion so a parent+-- binder never collides with a child's.+--+-- @since 1.7.0.0+buildInlineParseClauses+ :: Quote m+ => (Exp -> Exp)+ -- ^ Wrap a child-route expression in the accumulated parent constructors.+ -> ResourceTree a+ -> m [Clause]+buildInlineParseClauses wrap resourceTree =+ map matchToClause <$> buildInlineParseMatches wrap resourceTree+ where+ -- Each parse match is a single @(path, queryParams)@ pattern with a body,+ -- which is exactly a top-level @parseRoute@ clause. Total by construction —+ -- 'Match' and 'Clause' carry the same body\/@where@ shape — so no partial+ -- destructure is needed.+ matchToClause :: Match -> Clause+ matchToClause (Match pat body decs) = Clause [pat] body decs++-- | The matching alternatives behind 'buildInlineParseClauses', one per+-- resource tree. Producing 'Match'es (rather than 'Clause's) is what lets a+-- parent splice its children straight into a @case@: a leaf is one+-- @(path, queryParams)@ alternative, and a parent is one alternative that+-- matches its path prefix then cases the remaining @(path, queryParams)@ over+-- its children's alternatives, ending in a @_ -> 'Nothing'@ fallback. At top+-- level 'buildInlineParseClauses' turns each alternative back into its own+-- @parseRoute@ clause.+buildInlineParseMatches+ :: Quote m+ => (Exp -> Exp)+ -- ^ Wrap a child-route expression in the accumulated parent constructors.+ -> ResourceTree a+ -> m [Match]+buildInlineParseMatches wrap resourceTree =+ case resourceTree of+ ResourceLeaf (Resource name pieces dispatch _ _check) -> do+ (pats, dyns) <- handlePiecesM pieces+ case dispatch of+ Methods multi _ -> do+ (finalTail, dyns') <-+ case multi of+ Nothing ->+ pure (EndExact, dyns)+ Just _ -> do+ multiName <- newName "multi"+ pure (EndMulti multiName, dyns ++ [VarE multiName])+ queryParamsName <- newName "_queryParams"+ let route = applyConPieces name dyns'+ jroute = ConE 'Just `AppE` wrap route+ pathPat = mkPathPat finalTail pats+ pat = TupP [pathPat, VarP queryParamsName]+ pure [Match pat (NormalB jroute) []]++ Subsite _ _ -> do+ restName <- newName "rest"+ queryParamsName <- newName "_queryParams"+ subName <- newName "sub"+ let route = applyConPieces name dyns+ wrapSub = wrap (route `AppE` VarE subName)+ pathPat = mkPathPat (EndRest restName) pats+ pat = TupP [pathPat, VarP queryParamsName]+ -- 'unsafeQToQuote' because brackets are monomorphic 'Q'+ -- on template-haskell < 2.17. It is safe here: the only+ -- effect a bracket performs is drawing hygiene names for+ -- its binders, and this one has none (the lambda binder+ -- comes from a splice).+ expr <- unsafeQToQuote [e|+ fmap+ (\ $(varP subName) -> $(pure wrapSub) )+ (parseRoute ( $(varE restName), $(varE queryParamsName) ) )+ |]+ pure [Match pat (NormalB expr) []]++ ResourceParent name _check _attrs pieces children -> do+ (pats, dyns) <- handlePiecesM pieces+ restName <- newName "rest"+ queryParamsName <- newName "_queryParams"+ let parentCon childRoute =+ applyConPieces name dyns `AppE` childRoute+ wrap' = wrap . parentCon++ -- Build each child's matching alternative relative to the remaining+ -- @(rest, queryParams)@ scope, then fold them into a single @case@+ -- that commits at this parent: a child miss falls to the+ -- @_ -> Nothing@ alternative instead of escaping to a sibling+ -- top-level route.+ childMatches <- concat <$> mapM (buildInlineParseMatches wrap') children+ let restTup = mkTupE [VarE restName, VarE queryParamsName]+ fallbackMatch = Match WildP (NormalB (ConE 'Nothing)) []+ caseExpr = CaseE restTup (childMatches ++ [fallbackMatch])+ pathPat = mkPathPat (EndRest restName) pats+ pat = TupP [pathPat, VarP queryParamsName]+ pure [Match pat (NormalB caseExpr) []]
src/Yesod/Routes/TH/RenderRoute.hs view
@@ -1,167 +1,979 @@-{-# LANGUAGE TemplateHaskell, CPP #-}-module Yesod.Routes.TH.RenderRoute- ( -- ** RenderRoute- mkRenderRouteInstance- , mkRouteCons- , mkRenderRouteClauses- ) where--import Yesod.Routes.TH.Types-import Language.Haskell.TH (conT)-import Language.Haskell.TH.Syntax-import Data.Bits (xor)-import Data.Maybe (maybeToList)-import Control.Monad (replicateM)-import Data.Text (pack)-import Web.PathPieces (PathPiece (..), PathMultiPiece (..))-import Yesod.Routes.Class---- | Generate the constructors of a route data type.-mkRouteCons :: [ResourceTree Type] -> Q ([Con], [Dec])-mkRouteCons rttypes =- mconcat <$> mapM mkRouteCon rttypes- where- mkRouteCon (ResourceLeaf res) =- return ([con], [])- where- con = NormalC (mkName $ resourceName res)- $ map (\x -> (notStrict, x))- $ concat [singles, multi, sub]- singles = concatMap toSingle $ resourcePieces res- toSingle Static{} = []- toSingle (Dynamic typ) = [typ]-- multi = maybeToList $ resourceMulti res-- sub =- case resourceDispatch res of- Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ]- _ -> []-- mkRouteCon (ResourceParent name _check pieces children) = do- (cons, decs) <- mkRouteCons children-#if MIN_VERSION_template_haskell(2,12,0)- dec <- DataD [] (mkName name) [] Nothing cons <$> fmap (pure . DerivClause Nothing) (mapM conT [''Show, ''Read, ''Eq])-#else- dec <- DataD [] (mkName name) [] Nothing cons <$> mapM conT [''Show, ''Read, ''Eq]-#endif- return ([con], dec : decs)- where- con = NormalC (mkName name)- $ map (\x -> (notStrict, x))- $ singles ++ [ConT $ mkName name]-- singles = concatMap toSingle pieces- toSingle Static{} = []- toSingle (Dynamic typ) = [typ]---- | Clauses for the 'renderRoute' method.-mkRenderRouteClauses :: [ResourceTree Type] -> Q [Clause]-mkRenderRouteClauses =- mapM go- where- isDynamic Dynamic{} = True- isDynamic _ = False-- go (ResourceParent name _check pieces children) = do- let cnt = length $ filter isDynamic pieces- dyns <- replicateM cnt $ newName "dyn"- child <- newName "child"- let pat = ConP (mkName name) $ map VarP $ dyns ++ [child]-- pack' <- [|pack|]- tsp <- [|toPathPiece|]- let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp pieces dyns-- childRender <- newName "childRender"- let rr = VarE childRender- childClauses <- mkRenderRouteClauses children-- a <- newName "a"- b <- newName "b"-- colon <- [|(:)|]- let cons y ys = InfixE (Just y) colon (Just ys)- let pieces' = foldr cons (VarE a) piecesSingle-- let body = LamE [TupP [VarP a, VarP b]] (TupE [pieces', VarE b]) `AppE` (rr `AppE` VarE child)-- return $ Clause [pat] (NormalB body) [FunD childRender childClauses]-- go (ResourceLeaf res) = do- let cnt = length (filter isDynamic $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)- dyns <- replicateM cnt $ newName "dyn"- sub <-- case resourceDispatch res of- Subsite{} -> return <$> newName "sub"- _ -> return []- let pat = ConP (mkName $ resourceName res) $ map VarP $ dyns ++ sub-- pack' <- [|pack|]- tsp <- [|toPathPiece|]- let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp (resourcePieces res) dyns-- piecesMulti <-- case resourceMulti res of- Nothing -> return $ ListE []- Just{} -> do- tmp <- [|toPathMultiPiece|]- return $ tmp `AppE` VarE (last dyns)-- body <-- case sub of- [x] -> do- rr <- [|renderRoute|]- a <- newName "a"- b <- newName "b"-- colon <- [|(:)|]- let cons y ys = InfixE (Just y) colon (Just ys)- let pieces = foldr cons (VarE a) piecesSingle-- return $ LamE [TupP [VarP a, VarP b]] (TupE [pieces, VarE b]) `AppE` (rr `AppE` VarE x)- _ -> do- colon <- [|(:)|]- let cons a b = InfixE (Just a) colon (Just b)- return $ TupE [foldr cons piecesMulti piecesSingle, ListE []]-- return $ Clause [pat] (NormalB body) []-- mkPieces _ _ [] _ = []- mkPieces toText tsp (Static s:ps) dyns = toText s : mkPieces toText tsp ps dyns- mkPieces toText tsp (Dynamic{}:ps) (d:dyns) = tsp `AppE` VarE d : mkPieces toText tsp ps dyns- mkPieces _ _ (Dynamic _ : _) [] = error "mkPieces 120"---- | Generate the 'RenderRoute' instance.------ This includes both the 'Route' associated type and the--- 'renderRoute' method. This function uses both 'mkRouteCons' and--- 'mkRenderRouteClasses'.-mkRenderRouteInstance :: Cxt -> Type -> [ResourceTree Type] -> Q [Dec]-mkRenderRouteInstance cxt typ ress = do- cls <- mkRenderRouteClauses ress- (cons, decs) <- mkRouteCons ress-#if MIN_VERSION_template_haskell(2,12,0)- did <- DataInstD [] ''Route [typ] Nothing cons <$> fmap (pure . DerivClause Nothing) (mapM conT (clazzes False))- let sds = fmap (\t -> StandaloneDerivD Nothing cxt $ ConT t `AppT` ( ConT ''Route `AppT` typ)) (clazzes True)-#else- did <- DataInstD [] ''Route [typ] Nothing cons <$> mapM conT (clazzes False)- let sds = fmap (\t -> StandaloneDerivD cxt $ ConT t `AppT` ( ConT ''Route `AppT` typ)) (clazzes True)-#endif- return $ instanceD cxt (ConT ''RenderRoute `AppT` typ)- [ did- , FunD (mkName "renderRoute") cls- ]- : sds ++ decs- where- clazzes standalone = if standalone `xor` null cxt then- clazzes'- else- []- clazzes' = [''Show, ''Eq, ''Read]--notStrict :: Bang-notStrict = Bang NoSourceUnpackedness NoSourceStrictness--instanceD :: Cxt -> Type -> [Dec] -> Dec-instanceD = InstanceD Nothing+{-# LANGUAGE CPP #-}+{-# LANGUAGE RecordWildCards #-}+-- 'TemplateHaskellQuotes' is not enough here: on GHC < 9.0 it does not+-- enable nested @$(...)@ splices inside brackets (they parse as the @$@+-- operator and trigger cross-stage 'Lift' errors).+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ViewPatterns #-}++module Yesod.Routes.TH.RenderRoute+ ( -- ** RenderRoute+ mkRenderRouteInstanceOpts+ , mkRouteConsOpts+ , shouldCreateResources++ , RouteOpts+ , defaultOpts+ , setEqDerived+ , setShowDerived+ , setReadDerived+ , setCreateResources+ , setFocusOnNestedRoute+ , unsetFocusOnNestedRoute+ , roFocusOnNestedRoute+ , roNestedRouteFallthrough+ , setParameterizedSubroute+ , setNestedRouteFallthrough+ , DiscoveryMode(..)+ , discoveryMode+ ) where++import Data.Maybe+import Yesod.Routes.TH.Types+import Language.Haskell.TH (varE, varP)+import Language.Haskell.TH.Syntax+import Control.Monad+import Data.Text (pack)+import Web.PathPieces (PathPiece (..), PathMultiPiece (..))+import Yesod.Routes.Class+import Data.Foldable+import Yesod.Routes.TH.Internal+import Data.Char+import Yesod.Core.Class.Dispatch.ToParentRoute++-- | General opts data type for generating yesod.+--+-- Contains options for customizing code generation for the router in+-- 'mkYesodData', including what type class instances will be derived for+-- the route datatype, whether to parameterize subroutes,+-- and whether or not to create the @resources :: [ResourceTree String]@ value.+-- Use the setting functions on `defaultOpts` to set specific fields.+--+-- @since 1.6.25.0+data RouteOpts = MkRouteOpts+ { roDerivedEq :: Bool+ , roDerivedShow :: Bool+ , roDerivedRead :: Bool+ , roCreateResources :: Bool+ , roFocusOnNestedRoute :: Maybe String+ -- ^ If this option is set, then we will only generate datatypes for+ -- the nested subroute that matches this string.+ --+ -- @since 1.7.0.0+ , roParameterizedSubroute :: Bool+ , roNestedRouteFallthrough :: Bool+ -- ^ If 'True', then a nested route will fall through if it fails to+ -- match. If 'False', then a nested route will throw 'notFound' if it+ -- does not match.+ --+ -- Default: 'False'.+ --+ -- @since 1.7.0.0+ }++-- | Default options for generating routes.+--+-- The defaults are:+--+-- * 'setEqDerived', 'setShowDerived', 'setReadDerived': 'True' — derive+-- 'Eq', 'Show' and 'Read' for the route datatype.+-- * 'setCreateResources': 'True' — emit the @resourcesSite ::+-- [ResourceTree String]@ term.+-- * 'setParameterizedSubroute': 'False' — subroutes are unparameterized.+-- * 'setFocusOnNestedRoute': unset — generate the whole route tree+-- rather than focusing on a single nested route.+-- * 'setNestedRouteFallthrough': 'False' — a nested route that fails to+-- match throws 'notFound' instead of falling through.+--+-- Use the @set*@ functions to override individual fields.+--+-- @since 1.6.25.0+defaultOpts :: RouteOpts+defaultOpts = MkRouteOpts+ { roDerivedEq = True+ , roDerivedShow = True+ , roDerivedRead = True+ , roCreateResources = True+ , roParameterizedSubroute = False+ , roFocusOnNestedRoute = Nothing+ , roNestedRouteFallthrough = False+ }++-- | If you set this with @routeName@, then the code generation will+-- generate code for the @routeName@ to be imported in the main dispatch+-- class. This allows you to generate code in separate modules.+--+-- Example:+--+-- First, you would put your route definitions into their own file.+--+-- @+-- module App.Routes.Resources where+--+-- import Yesod.Core+--+-- appResources :: [ResourceTree String]+-- appResources = [parseRoutes|+-- / HomeR GET+--+-- /nest NestR:+-- / NestIndexR GET POST+-- /#Int NestShowR GET POST+--+-- |]+-- @+--+-- We have defined a nested route called @NestR@ here. A nested route is+-- created with the @:@ after the route name.+--+-- Then, in a module for the route specifically, we can generate the route+-- datatype and instances for later hooking in to the main instance.+--+-- @+-- module App.Routes.NestR where+--+-- import App.Routes.Resources+-- import Yesod.Core+--+-- mkYesodOpts (setFocusOnNestedRoute "NestR" defaultOpts) "App" appResources+-- @+--+-- If you only want to generate the datatypes, you can separate things+-- further using 'mkYesodDataOpts' and 'mkYesodDispatchOpts'.+--+-- Finally, import that type into your main yesod macro code.+--+-- @+-- module App where+--+-- import App.Routes.Resources (appResources)+-- import App.Routes.NestR (NestR(..))+--+-- mkYesod "App" appResources+-- @+--+-- The call to 'mkYesod' will delegate to the generated code for @NestR@+-- rather than regenerating it.+--+-- By default, 'defaultOpts' starts unfocused (the whole route tree is+-- generated). Use 'unsetFocusOnNestedRoute' to clear a previously-set focus.+--+-- For a full walkthrough — including subsites, linking to nested routes, and+-- why split sites should enable 'setNestedRouteFallthrough' — see the guide at+-- <https://github.com/yesodweb/yesod/blob/master/yesod-core/docs/split-route-compilation.md>.+--+-- @since 1.7.0.0+setFocusOnNestedRoute :: String -> RouteOpts -> RouteOpts+setFocusOnNestedRoute str rdo = rdo { roFocusOnNestedRoute = Just str }++-- | Clear any nested-route focus set by 'setFocusOnNestedRoute', so that+-- the whole route tree is generated.+--+-- 'defaultOpts' already starts unfocused, so this is only needed to undo a+-- previous 'setFocusOnNestedRoute'.+--+-- @since 1.7.0.0+unsetFocusOnNestedRoute :: RouteOpts -> RouteOpts+unsetFocusOnNestedRoute rdo = rdo { roFocusOnNestedRoute = Nothing }++-- | When 'True', a nested route that fails to match falls through to the+-- enclosing dispatch rather than throwing 'notFound'. When 'False', a+-- non-matching nested route throws 'notFound'.+--+-- Default: 'False'.+--+-- @since 1.7.0.0+setNestedRouteFallthrough :: Bool -> RouteOpts -> RouteOpts+setNestedRouteFallthrough b rdo = rdo { roNestedRouteFallthrough = b }++-- | When 'True', derive an 'Eq' instance for the route datatype.+--+-- Default: 'True'.+--+-- @since 1.6.25.0+setEqDerived :: Bool -> RouteOpts -> RouteOpts+setEqDerived b rdo = rdo { roDerivedEq = b }++-- | When 'True', derive a 'Show' instance for the route datatype.+--+-- Default: 'True'.+--+-- @since 1.6.25.0+setShowDerived :: Bool -> RouteOpts -> RouteOpts+setShowDerived b rdo = rdo { roDerivedShow = b }++-- | When 'True', derive a 'Read' instance for the route datatype.+--+-- Default: 'True'.+--+-- @since 1.6.25.0+setReadDerived :: Bool -> RouteOpts -> RouteOpts+setReadDerived b rdo = rdo { roDerivedRead = b }++-- | Determine whether or not to generate the @resourcesApp@ value.+--+-- Disabling this can be useful if you are creating the @routes ::+-- [ResourceTree String]@ elsewhere in your module, and referring to it+-- here. The @resourcesApp@ can become very large in large applications,+-- and duplicating it can result in signifiacntly higher compile times.+--+-- Default: 'True'.+--+-- @since 1.6.28.0+setCreateResources :: Bool -> RouteOpts -> RouteOpts+setCreateResources b rdo = rdo { roCreateResources = b }++-- | Returns whether or not we should create the @resourcesSite ::+-- [ResourceTree String]@ value during code generation.+--+-- @since 1.6.28.0+shouldCreateResources :: RouteOpts -> Bool+shouldCreateResources = roCreateResources++-- | If True, generate nested-discovery code (separate subroute datatypes and+-- @RenderRouteNested@ / dispatch instances) for a /parameterized/ site instead+-- of the backwards-compatible inline output. The subroute datatypes then carry+-- the parent site's type variables so that the @ParentSite@ \/ @ParentArgs@+-- associated types stay well-scoped.+--+-- Because the generated nested instances are parameterized over the site's+-- type variables, the splice emits instance heads with non-variable arguments;+-- a module using this typically needs @FlexibleContexts@, @FlexibleInstances@,+-- @MultiParamTypeClasses@, @TypeFamilies@ and, for a parameterized subsite+-- whose @master@ is determined by the @subsite@ (a @subsite -> master@+-- functional dependency on the user's class), @UndecidableInstances@.+--+-- Monomorphic sites always use nested discovery regardless of this flag; see+-- 'discoveryMode'.+--+-- Default: 'False'.+--+-- @since 1.6.28.0+setParameterizedSubroute :: Bool -> RouteOpts -> RouteOpts+setParameterizedSubroute b rdo = rdo { roParameterizedSubroute = b }++-- |+--+-- @since 1.6.25.0+instanceNamesFromOpts :: RouteOpts -> [Name]+instanceNamesFromOpts MkRouteOpts {..} = prependIf roDerivedEq ''Eq $ prependIf roDerivedShow ''Show $ prependIf roDerivedRead ''Read []+ where prependIf b = if b then (:) else const id++-- | How a route datatype's children are generated.+--+-- @since 1.7.0.0+data DiscoveryMode+ = -- | The backwards-compatible default for a /parameterized/ site that has+ -- not opted in: subroute datatypes stay unparameterized (kind 'Type')+ -- and @renderRoute@ \/ @parseRoute@ \/ dispatch inline the children,+ -- exactly as pre-nested-discovery Yesod did. Generating the discovery+ -- machinery here would put the parent's type variables out of scope on+ -- the child datatype, so we don't.+ InlineCompat+ | -- | Generate the "nested route discovery" machinery: parameterized+ -- subroute datatypes plus the @RenderRouteNested@ \/ @ParseRouteNested@+ -- \/ @YesodDispatchNested@ \/ @ToParentRoute@ \/ @UrlToDispatch@ \/+ -- @RedirectUrl@ instances and the delegating method bodies that go with+ -- them.+ NestedDiscovery+ deriving (Eq, Show)++-- | Classify how a route datatype's children should be generated, given the+-- site's route options and its 'TyArgs'.+--+-- 'NestedDiscovery' is chosen when any of the following hold:+--+-- * The site has /no/ type parameters ('hasTyArgs' is 'False', e.g. a+-- monomorphic site like @App@). Subroute datatypes then need no type+-- variables, so the machinery is always safe, and this preserves the+-- historical behaviour of monomorphic sites (including splitting nested+-- routes across modules).+--+-- * Parameterized subroutes were explicitly requested+-- ('setParameterizedSubroute'). The subroute datatypes then carry the+-- parent's type variables so the @ParentSite@ \/ @ParentArgs@ associated+-- types are well-scoped.+--+-- * We are focusing on a nested route for module-splitting+-- ('setFocusOnNestedRoute').+--+-- @since 1.7.0.0+discoveryMode :: RouteOpts -> TyArgs -> DiscoveryMode+discoveryMode opts tyargs+ | not (hasTyArgs tyargs) = NestedDiscovery+ | roParameterizedSubroute opts = NestedDiscovery+ | isJust (roFocusOnNestedRoute opts) = NestedDiscovery+ | otherwise = InlineCompat++-- | 'PlainTV' across template-haskell versions. @th-abstraction@ covers this,+-- but the version it was introduced in isn't always available — and the+-- 'TyVarBndr' /flag/ type also varies by version, so the result type is+-- CPP-guarded too. Used to build the type-variable binders of generated+-- subroute datatypes.+plainTVCompat :: Name ->+#if MIN_VERSION_template_haskell(2,21,0)+ TyVarBndr BndrVis+#elif MIN_VERSION_template_haskell(2,17,0)+ TyVarBndr ()+#else+ TyVarBndr+#endif+plainTVCompat =+#if MIN_VERSION_template_haskell(2,21,0)+ (`PlainTV` BndrReq)+#elif MIN_VERSION_template_haskell(2,17,0)+ (`PlainTV` ())+#else+ PlainTV+#endif++-- | Build the data constructor for a route leaf: @LeafR ty… [multi] [Route Sub]@.+-- Shared by the full-tree ('mkRouteConsOpts') and focused-nested+-- ('mkRenderRouteNestedInstanceOpts') constructor generators, which previously+-- spelled this identically.+leafRouteCon :: Resource Type -> Con+leafRouteCon res =+ NormalC (mkName $ resourceName res)+ $ map (notStrict,)+ $ concat [singles, multi, sub]+ where+ singles = concatMap toSingle $ resourcePieces res+ toSingle Static{} = []+ toSingle (Dynamic typ) = [typ]+ multi = maybeToList $ resourceMulti res+ sub =+ case resourceDispatch res of+ Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ]+ _ -> []++-- | Build the data constructor for a route parent: @ParentR ty… (ChildR tyargs…)@,+-- whose trailing field is the (possibly type-argument-applied) child route+-- datatype. Shared by the full-tree and focused-nested constructor generators.+parentRouteCon :: String -> [Piece Type] -> TyArgs -> Con+parentRouteCon name pieces tyargs =+ NormalC (mkName name)+ $ map (notStrict,)+ $ singles ++ [applyTyArgs (ConT (mkName name)) tyargs]+ where+ singles = concatMap toSingle pieces+ toSingle Static{} = []+ toSingle (Dynamic typ) = [typ]++-- | The deriving context for a child\/subroute datatype. The site's instance+-- context only makes sense on a child that actually carries the type+-- arguments; an unparameterized child (the backwards-compatible default)+-- can't, so deriving with that context would leave the variables ambiguous.+-- Nullify the context in that case, mirroring historical Yesod.+childDerivCxt :: TyArgs -> Cxt -> Cxt+childDerivCxt tyargs cxt+ | hasTyArgs tyargs = cxt+ | otherwise = []++-- | Generate the constructors of a route data type, with custom+-- 'RouteOpts'.+--+-- The first element in the return is the list of constructors for the+-- @Route site@ datatype. The second element is the list of 'Dec' to+-- declare nested datatypes or class instances of the 'RenderRouteNested'+-- for those instances.+--+-- @since 1.6.25.0+mkRouteConsOpts :: RouteOpts -> Cxt -> TyArgs -> Type -> [ResourceTree Type] -> Q ([Con], [Dec])+mkRouteConsOpts opts cxt origTyargs master resourceTrees = do+ (prePieces, focusedTrees) <-+ case roFocusOnNestedRoute opts of+ Nothing ->+ pure ([], resourceTrees)+ Just target -> do+ let mnestedRoute = findNestedRoute target resourceTrees+ case mnestedRoute of+ Nothing ->+ fail $ "Target '" <> target <> "' was not found in resources."+ Just r ->+ pure r+ mkRouteConsOpts' prePieces focusedTrees+ where+ -- When nested route discovery is enabled, child datatypes carry the+ -- parent's type variables, because RenderRouteNested has associated type+ -- families (ParentSite, ParentArgs) that reference the parent type;+ -- without the type variables on the child datatype, they'd be out of+ -- scope on the RHS of the type family instance.+ --+ -- When it is disabled (the backwards-compatible default), child+ -- datatypes are unparameterized (kind 'Type'), matching the historical+ -- output. See 'discoveryMode'. Computed once and reused below.+ mode = discoveryMode opts origTyargs+ tyargs =+ case mode of+ NestedDiscovery -> origTyargs+ InlineCompat -> NoTyArgs+ subrouteDecTypeArgs = fmap plainTVCompat (tyArgsBinders tyargs)++ -- Derives for the child (subroute) datatypes. See 'childDerivCxt'.+ childCxt = childDerivCxt tyargs cxt+ (inlineDerives, mkStandaloneDerives) = getDerivesFor opts childCxt++ mkRouteConsOpts' :: [Piece Type] -> [ResourceTree Type] -> Q ([Con], [Dec])+ mkRouteConsOpts' prePieces trees = do+ results <- mapM (mkRouteCon prePieces) trees+ pure (mconcat results)++ mkRouteCon :: [Piece Type] -> ResourceTree Type -> Q ([Con], [Dec])+ mkRouteCon _ (ResourceLeaf res) =+ -- A leaf is always a constructor of the (possibly focused) datatype:+ -- in focus mode we have already narrowed @resourceTrees@ to the+ -- target's children, so every leaf reached here is a leaf of the+ -- focused route.+ pure ([leafRouteCon res], [])++ mkRouteCon prePieces (ResourceParent name _check _attrs pieces children) = do+ -- Accumulate pieces for children: combine parent pieces with this route's pieces+ let accumulatedPieces = prePieces <> pieces+ (cons, decs) <- mkRouteConsOpts' accumulatedPieces children+ let childDataName = mkName name++ -- Generate the child datatype if it has not been generated+ -- already, but only if we are *not* focusing on some other+ -- datatype.+ mname' <- lookupTypeName name+ mdec <- case mname' of+ Just _ -> do+ -- datatype already exists, definitely don't generate it+ pure Nothing+ Nothing -> do+ let childData = mkChildDataGen childDataName cons inlineDerives+ childDataType = applyTyArgs (ConT childDataName) tyargs+ case roFocusOnNestedRoute opts of+ Just target | target /= name ->+ -- If we have a target, and this ain't it, don't+ -- generate+ pure Nothing+ _ | InlineCompat <- mode ->+ -- Backwards-compatible default: emit just the+ -- (unparameterized) child datatype and its standalone+ -- derives. No RenderRouteNested instance; renderRoute+ -- inlines the children.+ pure $ Just (childData : mkStandaloneDerives childDataType)+ _ ->+ -- Nested discovery: either `Just target | target ==+ -- name` (matching a target, and this is it) or+ -- `Nothing` (no target, or we already found it). Emit+ -- the child datatype plus its RenderRouteNested+ -- instance so the parent's renderRoute can delegate.+ Just <$>+ nestedChildDataAndInstance+ opts cxt childCxt tyargs master childDataName cons+ (prePieces <> pieces) children++ return ([parentRouteCon name pieces tyargs], maybe id (<>) mdec decs)+ where+ mkChildDataGen :: Name -> [Con] -> [DerivClause] -> Dec+ mkChildDataGen childDataName cons conts =+ DataD [] childDataName subrouteDecTypeArgs Nothing cons conts++-- | The shared preamble for rendering a 'ResourceParent': fresh names for the+-- parent's dynamic pieces and for the wrapped @child@ route, plus the+-- constructor pattern @Name dyn… child@ binding them. Returns the dynamic+-- names, the child name, and the pattern.+parentConPat :: String -> [Piece a] -> Q ([Name], Name, Pat)+parentConPat name pieces = do+ let cnt = length [() | Dynamic{} <- pieces]+ dyns <- replicateM cnt $ newName "dyn"+ child <- newName "child"+ pure (dyns, child, conPCompat (mkName name) $ map VarP $ dyns ++ [child])++-- | Is this path piece a dynamic capture, as opposed to a static segment?+isDynamic :: Piece t -> Bool+isDynamic Dynamic{} = True+isDynamic _ = False++-- | Render a route's path pieces to a list of @Text@-valued expressions,+-- pairing each 'Dynamic' piece with its bound variable in order. Fails the+-- splice if the piece list and the binder list are out of sync (a dynamic+-- piece with no corresponding bound variable).+mkPieces :: (String -> Exp) -> Exp -> [Piece t] -> [Name] -> Q [Exp]+mkPieces _ _ [] _ = pure []+mkPieces toText tsp (Static s:ps) dyns = (toText s :) <$> mkPieces toText tsp ps dyns+mkPieces toText tsp (Dynamic{}:ps) (d:dyns) = (tsp `AppE` VarE d :) <$> mkPieces toText tsp ps dyns+mkPieces _ _ (Dynamic _ : _) [] =+ fail "RenderRoute.mkPieces: a dynamic path piece has no corresponding bound variable (route definition and piece-binder list are out of sync)"++-- | Clauses for the 'renderRoute' method. This should be called from the+-- instance derivation for 'RenderRoute'.+mkRenderRouteClauses :: RouteOpts -> TyArgs -> [ResourceTree Type] -> Q [Clause]+mkRenderRouteClauses opts origTyargs =+ goList+ where+ goList = fmap mconcat . mapM go++ mode = discoveryMode opts origTyargs++ go (ResourceParent name _check _attrs pieces children) =+ case mode of+ -- Opted in to nested discovery: the parent's renderRoute delegates+ -- to the child's RenderRouteNested instance.+ NestedDiscovery -> goNested+ -- Backwards-compatible default: inline the child clauses,+ -- prepending this parent's path pieces, exactly as historical+ -- Yesod did.+ InlineCompat -> goInline+ where+ goNested = do+ (dyns, child, pat) <- parentConPat name pieces++ body <- [| renderRouteNested $(pure (parentArgsExpr dyns)) $(pure (VarE child)) |]+ pure [Clause [pat] (NormalB body) []]++ goInline = do+ (dyns, child, pat) <- parentConPat name pieces++ pack' <- [|pack|]+ tsp <- [|toPathPiece|]+ piecesSingle <- mkPieces (AppE pack' . LitE . StringL) tsp pieces dyns++ childRender <- newName "childRender"+ childClauses <- goList children++ body <- delegatingBody piecesSingle (VarE childRender) (VarE child)++ pure [Clause [pat] (NormalB body) [FunD childRender childClauses]]++ go (ResourceLeaf res) = do+ let cnt = length (filter isDynamic $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)+ dyns <- replicateM cnt $ newName "dyn"+ sub <-+ case resourceDispatch res of+ Subsite{} -> return <$> newName "sub"+ _ -> return []+ let pat = conPCompat (mkName $ resourceName res) $ map VarP $ dyns ++ sub++ pack' <- [|pack|]+ tsp <- [|toPathPiece|]+ piecesSingle <- mkPieces (AppE pack' . LitE . StringL) tsp (resourcePieces res) dyns++ piecesMulti <-+ case resourceMulti res of+ Nothing -> return $ ListE []+ Just{} -> do+ tmp <- [|toPathMultiPiece|]+ return $ tmp `AppE` VarE (last dyns)++ body <-+ case sub of+ [x] -> do+ rr <- [|renderRoute|]+ delegatingBody piecesSingle rr (VarE x)+ _ ->+ return $ mkTupE [foldr consE piecesMulti piecesSingle, ListE []]++ return [Clause [pat] (NormalB body) []]++-- | Build a renderRoute body that delegates its tail to another render+-- function — either a nested child's @renderRouteNested@ (the inline+-- 'ResourceParent' arm) or an embedded subsite's @renderRoute@ (the+-- 'ResourceLeaf' subsite arm). The path-piece list passed in is prepended to+-- the delegate's path list (callers include any parent pieces themselves) and+-- the query string is threaded through unchanged.+delegatingBody+ :: [Exp] -- ^ this route's own path pieces (already including any parent pieces)+ -> Exp -- ^ the delegate render function+ -> Exp -- ^ the child/subsite route argument to render+ -> Q Exp+delegatingBody piecesSingle rr childArg = do+ a <- newName "a"+ b <- newName "b"+ let pieces' = foldr consE (VarE a) piecesSingle+ [e| ( \ ( $(varP a), $(varP b) ) -> ( $(pure pieces'), $(varE b) ) )+ ( $(pure rr) $(pure childArg) )+ |]++-- | Like 'mkRenderRouteClauses', but instead generates clauses for the+-- definition of 'renderRouteNested'.+--+-- @since 1.7.0.0+mkRenderRouteNestedClauses+ :: [Either String Name]+ -- ^ Either the static path piece or the names of the tuple of the parent arg.+ --+ -- These are both necessary to re-synthesize the total route.+ -> [ResourceTree Type]+ -> Q [Clause]+mkRenderRouteNestedClauses parentArgsNames resources = do+ fmap mconcat . mapM go $ resources+ where+ go (ResourceParent name _check _attrs pieces _children) = do+ (dyns, child, pat) <- parentConPat name pieces++ -- Extract parent dynamic variables from parentArgsNames+ let parentDyns = mapMaybe (either (const Nothing) Just) parentArgsNames++ -- Accumulate parent args: combine parent dynamic vars with this route's dynamic vars+ let allDyns = parentDyns ++ dyns+ accumulatedParentArgsExp = parentArgsExpr allDyns++ -- The body should call renderRouteNested with accumulated parent args+ body <- [| renderRouteNested $(pure accumulatedParentArgsExp) $(pure (VarE child)) |]+ pure [Clause [parentArgsPat parentDyns, pat] (NormalB body) []]++ go (ResourceLeaf res) = do+ let cnt = length (filter isDynamic $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)+ dyns <- replicateM cnt $ newName "dyn"+ sub <-+ case resourceDispatch res of+ Subsite{} -> return <$> newName "sub"+ _ -> return []+ let pat = conPCompat (mkName $ resourceName res) $ map VarP $ dyns ++ sub++ pack' <- [|pack|]+ tsp <- [|toPathPiece|]++ -- Build the FULL path including parent pieces+ -- parentArgsNames contains Either String Name for each piece in the parent path+ let parentDyns = mapMaybe (either (const Nothing) Just) parentArgsNames++ -- Build parent path pieces from parentArgsNames+ parentPieces <- mkParentPieces pack' tsp parentArgsNames parentDyns+ -- Build this resource's own pieces+ thisResourcePieces <- mkPieces (AppE pack' . LitE . StringL) tsp (resourcePieces res) dyns++ piecesMulti <-+ case resourceMulti res of+ Nothing -> return $ ListE []+ Just{} -> do+ tmp <- [|toPathMultiPiece|]+ return $ tmp `AppE` VarE (last dyns)++ body <-+ case sub of+ [x] -> do+ rr <- [|renderRoute|]+ -- Combine parent pieces with this resource's pieces+ delegatingBody (parentPieces ++ thisResourcePieces) rr (VarE x)+ _ -> do+ colon <- [|(:)|]+ let cons a b = InfixE (Just a) colon (Just b)+ -- Combine parent pieces with this resource's pieces+ let allPieces = parentPieces ++ thisResourcePieces+ return $ mkTupE [foldr cons piecesMulti allPieces, ListE []]++ return [Clause [parentArgsPat parentDyns, pat] (NormalB body) []]++ -- Build path pieces from parentArgsNames (which contains both static and dynamic pieces)+ mkParentPieces :: Exp -> Exp -> [Either String Name] -> [Name] -> Q [Exp]+ mkParentPieces pack' tsp parentArgsNames' parentDyns = goParentPieces parentArgsNames' parentDyns+ where+ goParentPieces [] _ = pure []+ goParentPieces (Left staticStr : rest) dyns =+ ((pack' `AppE` LitE (StringL staticStr)) :) <$> goParentPieces rest dyns+ goParentPieces (Right _ : rest) (dyn : dyns) =+ ((tsp `AppE` VarE dyn) :) <$> goParentPieces rest dyns+ goParentPieces (Right _ : _) [] =+ fail "RenderRoute.mkParentPieces: a dynamic parent path piece has no corresponding bound variable"++-- | The shared core of every 'RenderRouteNested' instance body. From a parent+-- route's accumulated path pieces and its child resources it computes the+-- @ParentArgs@ tuple 'Type' and the @renderRouteNested@ method clauses. The+-- three instance-emitting sites (the inline child datatype in+-- 'mkRenderRouteInstanceOpts', the focused target in+-- 'mkRenderRouteNestedInstanceOpts', and that target's nested children) differ+-- only in the instance head, the @ParentSite@ right-hand side, and the context+-- they wrap around this — so only those wrappers remain at the call sites, and+-- the piece-naming\/clause-building boilerplate (including the dynamic-binder+-- name derivation) lives here once.+renderRouteNestedBody+ :: [Piece Type] -- ^ accumulated parent path pieces+ -> [ResourceTree Type] -- ^ child resources+ -> Q (Type, [Clause]) -- ^ (ParentArgs tuple type, renderRouteNested clauses)+renderRouteNestedBody prepieces children = do+ let piecesAndNames =+ map (\p -> case p of+ Static str -> Left str+ Dynamic a -> Right a)+ prepieces+ preDyns = mapMaybe (either (const Nothing) Just) piecesAndNames+ let parentDynT = parentArgsType preDyns+ parentNames <- forM piecesAndNames $ \epiece'name ->+ case epiece'name of+ Left piece -> pure (Left piece)+ Right t ->+ case filter isAlphaNum (show t) of+ (a : as) -> Right <$> newName (toLower a : as)+ [] -> fail+ "renderRouteNested: a dynamic piece's type renders with no alphanumeric characters, so no binder name can be derived from it"+ childClauses <- mkRenderRouteNestedClauses parentNames children+ pure (parentDynT, childClauses)++-- | Generate the 'RenderRoute' instance.+--+-- This includes both the 'Route' associated type and the+-- 'renderRoute' method. This function uses both 'mkRouteConsOpts' and+-- 'mkRenderRouteClauses'.+--+-- @since 1.6.25.0+mkRenderRouteInstanceOpts+ :: RouteOpts+ -> Cxt+ -- ^ The context passed around for the instances+ -> TyArgs+ -- ^ Type arguments+ -> Type+ -- ^ The type of the foundation resource+ -> [ResourceTree Type]+ -- ^ The actual tree of routes to generate code for+ -> Q [Dec]+mkRenderRouteInstanceOpts opts cxt tyargs typ ress = do+ case roFocusOnNestedRoute opts of+ Nothing -> do+ cls <- mkRenderRouteClauses opts tyargs ress+ (cons, decs) <- mkRouteConsOpts opts cxt tyargs typ ress+ let did = DataInstD []+#if MIN_VERSION_template_haskell(2,15,0)+ Nothing routeDataName+#else+ ''Route [typ]+#endif+ Nothing cons inlineDerives+ -- ToParentRoute instances are only needed by the nested-discovery+ -- machinery; the backwards-compatible default emits none.+ parentRouteInstancesDecs <-+ case discoveryMode opts tyargs of+ NestedDiscovery -> mkToParentRouteInstances cxt tyargs ress+ InlineCompat -> pure []+ pure $ mconcat+ [ pure $ instanceD cxt (ConT ''RenderRoute `AppT` typ)+ [ did+ , FunD 'renderRoute cls+ ]+ , mkStandaloneDerives routeDataName+ , decs+ , parentRouteInstancesDecs+ ]+ Just target ->+ case findNestedRoute target ress of+ Nothing ->+ fail $ "Target '" <> target <> "' was not found in resources."+ Just (prepieces, ress') ->+ mkRenderRouteNestedInstanceOpts opts cxt tyargs typ prepieces target ress'+ where+ routeDataName = ConT ''Route `AppT` typ+ (inlineDerives, mkStandaloneDerives) = getDerivesFor opts cxt++-- | Get the simple derivation clauses and the standalone derivation clauses+-- for a given type and context.+--+-- If there are any additional classes needed for context, we just produce standalone+-- clauses. Else, we produce basic deriving clauses for a declaration.+getDerivesFor :: RouteOpts -> Cxt -> ([DerivClause], Type -> [Dec])+getDerivesFor opts cxt+ | null cxt =+ ( [DerivClause Nothing clazzes']+ , const []+ )+ | otherwise =+ ( []+ , \typ -> fmap (StandaloneDerivD Nothing cxt . (`AppT` typ)) clazzes'+ )+ where+ clazzes' = ConT <$> instanceNamesFromOpts opts++-- | For each datatype, generate an instance of 'ToParentRoute' for the+-- datatype. Instances should mostly look like this:+--+-- > instance ToParentRoute FooR where+-- > toParentRoute (a0, a1) = FooR a0 a1+mkToParentRouteInstances :: Cxt -> TyArgs -> [ResourceTree Type] -> Q [Dec]+mkToParentRouteInstances cxt origTyargs ress = do+ mconcat <$> mapM (go ([], [])) ress+ where+ go _ (ResourceLeaf _) =+ pure []+ go (accPieces, parentConstructors) (ResourceParent name _check _attrs pieces children) = do+ -- Extract dynamic types from accumulated parent pieces+ let accDynTypes = [t | Dynamic t <- accPieces]+ accDynVars <- mapM (\_ -> newName "parent") accDynTypes++ -- Extract dynamic types from this route's pieces+ let piecesDynTypes = [t | Dynamic t <- pieces]+ piecesDynVars <- mapM (\_ -> newName "piece") piecesDynTypes++ -- Child route variable+ child <- newName "child"++ -- ParentArgs includes BOTH acc and pieces dynamic vars+ let allParentDynVars = accDynVars ++ piecesDynVars++ -- Build the full route by applying all parent constructors+ -- We need to partition allParentDynVars according to each constructor's piece count+ let applyConToParentArgs = buildRouteExpr parentConstructors (mkName name) allParentDynVars (VarE child)++ let toParentRouteD =+ FunD 'toParentRoute+ [ Clause+ [parentArgsPat allParentDynVars, VarP child]+ (NormalB applyConToParentArgs)+ []+ ]++ let thisInstance =+ instanceD cxt (ConT ''ToParentRoute `AppT` applyTypeVariables name) [toParentRouteD]++ -- Accumulate pieces and constructor info for children+ let thisPieceCount = length piecesDynTypes+ acc' =+ ( accPieces <> pieces+ , parentConstructors ++ [(mkName name, thisPieceCount)]+ )++ childrenInstances <- mconcat <$> mapM (go acc') children+ pure $ thisInstance : childrenInstances++ applyTypeVariables name =+ applyTyArgs (ConT (mkName name)) origTyargs++ -- Build the route expression by applying constructors from outermost to innermost+ buildRouteExpr :: [(Name, Int)] -> Name -> [Name] -> Exp -> Exp+ buildRouteExpr parentCtors thisCtor allDynVars childExpr =+ let -- All constructors including this one+ thisPieceCount = length allDynVars - sum (map snd parentCtors)+ allCtors = parentCtors ++ [(thisCtor, thisPieceCount)]++ -- Partition dynamic vars for each constructor+ partitionedVars = go' allDynVars allCtors+ where+ go' _ [] = []+ go' vars ((_, count) : rest) =+ let (varsForThis, remaining) = splitAt count vars+ in varsForThis : go' remaining rest++ -- Build expression from inside out using foldr+ finalExpr = foldr+ (\(ctorName, varsForThis) innerExpr ->+ foldl' AppE (ConE ctorName) (map VarE varsForThis ++ [innerExpr]))+ childExpr+ (zip (map fst allCtors) partitionedVars)+ in finalExpr++notStrict :: Bang+notStrict = Bang NoSourceUnpackedness NoSourceStrictness++-- | Assemble a @RenderRouteNested@ instance from the pieces that vary between+-- the three sites that emit one (the inline child datatype in+-- 'mkRouteConsOpts', the focused target and its nested children in+-- 'mkRenderRouteNestedInstanceOpts'). The context, child route 'Type',+-- @ParentSite@ right-hand side (the foundation 'Type') and @ParentArgs@ tuple+-- 'Type' plus the @renderRouteNested@ clauses are all that differ; everything+-- else is shared here.+--+-- @since 1.7.0.0+mkRenderRouteNestedInstanceD+ :: Cxt -- ^ instance context+ -> Type -- ^ the child route datatype (instance head argument)+ -> Type -- ^ the @ParentSite@ right-hand side (the foundation type)+ -> Type -- ^ the @ParentArgs@ tuple type+ -> [Clause] -- ^ the @renderRouteNested@ method clauses+ -> Dec+mkRenderRouteNestedInstanceD instCxt childRouteType parentSiteRHS parentArgsT clauses =+ instanceD+ instCxt+ (ConT ''RenderRouteNested `AppT` childRouteType)+ [ TySynInstD $ TySynEqn Nothing (ConT ''ParentSite `AppT` childRouteType) parentSiteRHS+ , TySynInstD $ TySynEqn Nothing (ConT ''ParentArgs `AppT` childRouteType) parentArgsT+ , FunD 'renderRouteNested clauses+ ]++-- | Generate the declarations for a nested-discovery /child/ datatype: the+-- (parameterized) datatype declaration, its @RenderRouteNested@ instance, and+-- any standalone deriving clauses. Shared by every nested-discovery+-- constructor generator ('mkRouteConsOpts' and 'mkRenderRouteNestedInstanceOpts')+-- so the datatype shape, instance context, and @ParentSite@\/@ParentArgs@ wiring+-- stay in one place. The focus\/inline gating that decides /whether/ to emit+-- these stays at the call sites, since that differs between them.+--+-- @since 1.7.0.0+nestedChildDataAndInstance+ :: RouteOpts+ -> Cxt -- ^ instance context for the @RenderRouteNested@ instance+ -> Cxt -- ^ deriving context for the child datatype (nullified when+ -- the child carries no type arguments; see 'mkRouteConsOpts')+ -> TyArgs -- ^ type arguments carried by the child datatype+ -> Type -- ^ the @ParentSite@ right-hand side (the foundation type)+ -> Name -- ^ the child datatype name+ -> [Con] -- ^ the child datatype's constructors+ -> [Piece Type] -- ^ accumulated parent path pieces for this child+ -> [ResourceTree Type] -- ^ the child's own children+ -> Q [Dec]+nestedChildDataAndInstance opts instCxt derivCxt tyargs parentSiteRHS childDataName cons accPieces children = do+ let subrouteDecTypeArgs = fmap plainTVCompat (tyArgsBinders tyargs)+ (inlineDerives, mkStandaloneDerives) = getDerivesFor opts derivCxt+ childData = DataD [] childDataName subrouteDecTypeArgs Nothing cons inlineDerives+ childDataType = applyTyArgs (ConT childDataName) tyargs+ (parentDynT, childClauses) <- renderRouteNestedBody accPieces children+ let childInstance =+ mkRenderRouteNestedInstanceD instCxt childDataType parentSiteRHS parentDynT childClauses+ pure (childData : childInstance : mkStandaloneDerives childDataType)++mkRenderRouteNestedInstanceOpts+ :: RouteOpts+ -> Cxt+ -> TyArgs+ -> Type+ -> [Piece Type]+ -> String+ -> [ResourceTree Type]+ -> Q [Dec]+mkRenderRouteNestedInstanceOpts routeOpts cxt tyargs typ prepieces target ress = do+ -- Generate constructors for all children+ (cons, childDecs) <- mkFocusedChildCons prepieces ress++ let targetName = mkName target+ targetDecs =+ -- The focused target datatype and its RenderRouteNested instance+ -- are emitted through the same shared builder as the in-module+ -- and nested-child paths, so the instance context is plain @cxt@+ -- (matching the in-module generator); demanding extra+ -- Eq/Show/Read constraints on the site type arguments here would+ -- be undischargeable at the delegating RenderRoute instance.+ nestedChildDataAndInstance+ routeOpts cxt childCxt tyargs typ targetName cons prepieces ress++ -- Return the datatype declaration, instance, and any standalone derives,+ -- followed by the declarations for any nested children.+ (<> childDecs) <$> targetDecs+ where+ -- The focused datatype carries the site's type arguments. See 'childDerivCxt'.+ childCxt = childDerivCxt tyargs cxt++ -- Generate constructors and nested-instance declarations for the children+ -- of the focused target. Structurally similar to 'mkRouteConsOpts'' inner+ -- @mkRouteCon@, but deliberately NOT shared: there we are walking the whole+ -- site and must honour the focus gate (skip datatypes other than the+ -- target) and the InlineCompat branch. Here every tree is already *inside*+ -- the focused target, so each parent unconditionally gets its+ -- RenderRouteNested instance — applying the focus gate here would wrongly+ -- skip grandchild instances.+ mkFocusedChildCons :: [Piece Type] -> [ResourceTree Type] -> Q ([Con], [Dec])+ mkFocusedChildCons prePieces trees =+ mconcat <$> mapM (mkFocusedChildCon prePieces) trees++ mkFocusedChildCon :: [Piece Type] -> ResourceTree Type -> Q ([Con], [Dec])+ mkFocusedChildCon _ (ResourceLeaf res) =+ pure ([leafRouteCon res], [])++ mkFocusedChildCon prePieces (ResourceParent name _check _attrs pieces children) = do+ let accumulatedPieces = prePieces <> pieces+ (cons, decs) <- mkFocusedChildCons accumulatedPieces children+ let childDataName = mkName name++ -- Generate the child datatype and its RenderRouteNested instance unless+ -- the datatype already exists (generated by another split-out module).+ mname' <- lookupTypeName name+ mdec <- case mname' of+ Just _ -> pure Nothing+ Nothing ->+ Just <$>+ nestedChildDataAndInstance+ routeOpts cxt childCxt tyargs typ childDataName cons+ accumulatedPieces children++ return ([parentRouteCon name pieces tyargs], maybe id (<>) mdec decs)
src/Yesod/Routes/TH/RouteAttrs.hs view
@@ -1,41 +1,117 @@-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+ module Yesod.Routes.TH.RouteAttrs ( mkRouteAttrsInstance+ , mkRouteAttrsInstanceFor+ , mkRouteAttrsNestedInstances ) where import Yesod.Routes.TH.Types+import Yesod.Routes.TH.Internal (nestedInstanceExists, resolveRouteCon, instanceD, conPCompat, findNestedRoute) import Yesod.Routes.Class import Language.Haskell.TH.Syntax-import Data.Set (fromList)-import Data.Text (pack)+import Data.Foldable (toList)+import qualified Data.Set as Set+import qualified Data.Text as Text+import Control.Monad mkRouteAttrsInstance :: Cxt -> Type -> [ResourceTree a] -> Q Dec mkRouteAttrsInstance cxt typ ress = do- clauses <- mapM (goTree id) ress+ clauses <- mapM (goTree Nothing id) ress return $ instanceD cxt (ConT ''RouteAttrs `AppT` typ)- [ FunD 'routeAttrs $ concat clauses+ [ FunD 'routeAttrs $ concat clauses +++ [Clause [ WildP ] (NormalB $ VarE 'mempty) []] ] -goTree :: (Pat -> Pat) -> ResourceTree a -> Q [Clause]-goTree front (ResourceLeaf res) = return <$> goRes front res-goTree front (ResourceParent name _check pieces trees) =- concat <$> mapM (goTree front') trees+-- | Like 'mkRouteAttrsInstance', but uses a 'String' name of a nested+-- subroute to generate nested instances instead of generating the full+-- instance for all routes.+--+-- @since 1.7.0.0+mkRouteAttrsInstanceFor :: Cxt -> Type -> String -> [ResourceTree a] -> Q [Dec]+mkRouteAttrsInstanceFor cxt typ target ress = do+ -- Fail loudly (matching Dispatch/RenderRoute/ParseRoute) when the focus+ -- target is missing, rather than silently emitting an always-'mempty'+ -- 'RouteAttrsNested' instance.+ case findNestedRoute target ress of+ Nothing ->+ fail $ "Target '" ++ target ++ "' was not found in resources."+ Just _ ->+ pure ()+ clauses <- mapM (goTree (Just target) id) ress+ return [instanceD cxt (ConT ''RouteAttrsNested `AppT` typ)+ [ FunD 'routeAttrsNested $ concat clauses +++ [Clause [ WildP ] (NormalB $ VarE 'mempty) []]+ ]+ ]++-- | Generate a 'RouteAttrsNested' instance for every nested parent route in+-- the tree (recursively), skipping any whose instance already exists (e.g. one+-- generated in another module when routes are split). This is the+-- 'RouteAttrs' counterpart to the @RenderRouteNested@ \/ @ParseRouteNested@ \/+-- @YesodDispatchNested@ instances that a plain 'mkYesod' already emits for its+-- child route fragments, so that @routeAttrsNested ChildR@ resolves for a+-- single-module site just like the other nested-delegation methods do.+--+-- @since 1.7.0.0+mkRouteAttrsNestedInstances :: Cxt -> TyArgs -> [ResourceTree a] -> Q [Dec]+mkRouteAttrsNestedInstances cxt tyargs ress =+ concat <$> mapM perParent (parentNames ress) where+ parentNames trees = concat+ [ name : parentNames children+ | ResourceParent name _ _ _ children <- trees+ ]+ perParent name = do+ exists <- nestedInstanceExists ''RouteAttrsNested =<< resolveRouteCon name+ if exists+ then pure []+ else mkRouteAttrsInstanceFor cxt (applyTyArgs (ConT (mkName name)) tyargs) name ress++goTree :: Maybe String -> (Pat -> Pat) -> ResourceTree a -> Q [Clause]+goTree mtarget front (ResourceLeaf res) =+ case mtarget of+ Nothing ->+ return $ toList $ goRes front res+ Just _ ->+ return []+goTree (Just target) front (ResourceParent name _check _attrs _pieces trees)+ | target /= name = concat <$> mapM (goTree (Just target) front) trees+goTree mtarget front (ResourceParent name _check _attrs pieces trees) = do+ doesNestedInstanceExist <- nestedInstanceExists ''RouteAttrsNested =<< resolveRouteCon name++ if doesNestedInstanceExist+ then do+ x <- newName "x"+ pure [Clause [front (conPCompat (mkName name) (ignored (VarP x)))] (NormalB $ VarE 'routeAttrsNested `AppE` VarE x) []]+ else do+ -- We only reach this clause when @mtarget@ is 'Nothing' (full+ -- instance) or @Just name@ (we found the focus target); clause 2+ -- above peeled off the non-matching @Just other@ case. Either way+ -- the remaining subtree is generated unfocused (@Nothing@). The+ -- focus target itself is the route boundary, so its own+ -- constructor is not prepended (@front@); ordinary parents in the+ -- full instance do prepend theirs (@front'@).+ let front'' =+ if Just name == mtarget+ then front+ else front'+ concat <$> mapM (goTree Nothing front'') trees+ where ignored = (replicate toIgnore WildP ++) . return toIgnore = length $ filter isDynamic pieces isDynamic Dynamic{} = True isDynamic Static{} = False- front' = front . ConP (mkName name) . ignored+ front' = front . conPCompat (mkName name)+ . ignored -goRes :: (Pat -> Pat) -> Resource a -> Q Clause-goRes front Resource {..} =+goRes :: (Pat -> Pat) -> Resource a -> Maybe Clause+goRes front Resource {..} = do+ guard (not (null resourceAttrs)) return $ Clause [front $ RecP (mkName resourceName) []]- (NormalB $ VarE 'fromList `AppE` ListE (map toText resourceAttrs))+ (NormalB $ VarE 'Set.fromList `AppE` ListE (map toText resourceAttrs)) [] where- toText s = VarE 'pack `AppE` LitE (StringL s)--instanceD :: Cxt -> Type -> [Dec] -> Dec-instanceD = InstanceD Nothing+ toText s = VarE 'Text.pack `AppE` LitE (StringL s)
src/Yesod/Routes/TH/Types.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveLift #-}+ -- | Warning! This module is considered internal and may have breaking changes module Yesod.Routes.TH.Types ( -- * Data types@@ -9,6 +12,16 @@ , Dispatch (..) , CheckOverlap , FlatResource (..)+ , ParentDetails (..)+ -- * Route datatype type arguments+ , TyArgs (..)+ , toTyArgs+ , tyArgsList+ , tyArgsTypes+ , tyArgsBinders+ , tyArgsArity+ , hasTyArgs+ , applyTyArgs -- ** Helper functions , resourceMulti , resourceTreePieces@@ -17,23 +30,88 @@ ) where import Language.Haskell.TH.Syntax+import Data.List (foldl')+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NonEmpty+import Data.Set (Set)+-- Provides Lift instance for Set in older versions of GHC+import Instances.TH.Lift () +-- | The type arguments of a route datatype, paired with the source 'Name' each+-- was derived from. A monomorphic site like @App@ has 'NoTyArgs'; a+-- parameterized one like @MySub subsite@ has 'SomeTyArgs'.+--+-- Carrying the (non-)emptiness in the type — rather than as a bare list with a+-- separate @null@ check — keeps \"are there type args?\" and \"what are they?\"+-- a single fact, so the populated branch hands you the 'NonEmpty' directly.+--+-- @since 1.7.0.0+data TyArgs+ = NoTyArgs+ | SomeTyArgs (NonEmpty (Type, Name))+ deriving (Eq, Show)++-- | Build 'TyArgs' from a list of (type, source-name) pairs.+--+-- @since 1.7.0.0+toTyArgs :: [(Type, Name)] -> TyArgs+toTyArgs [] = NoTyArgs+toTyArgs (x : xs) = SomeTyArgs (x :| xs)++-- | The type arguments as a plain list (empty for 'NoTyArgs').+--+-- @since 1.7.0.0+tyArgsList :: TyArgs -> [(Type, Name)]+tyArgsList NoTyArgs = []+tyArgsList (SomeTyArgs ne) = NonEmpty.toList ne++-- | Just the 'Type's of the arguments, in order.+--+-- @since 1.7.0.0+tyArgsTypes :: TyArgs -> [Type]+tyArgsTypes = map fst . tyArgsList++-- | Just the source 'Name's of the arguments, in order.+--+-- @since 1.7.0.0+tyArgsBinders :: TyArgs -> [Name]+tyArgsBinders = map snd . tyArgsList++-- | How many type arguments there are.+--+-- @since 1.7.0.0+tyArgsArity :: TyArgs -> Int+tyArgsArity NoTyArgs = 0+tyArgsArity (SomeTyArgs ne) = NonEmpty.length ne++-- | Whether there are any type arguments (i.e. the site is parameterized).+--+-- @since 1.7.0.0+hasTyArgs :: TyArgs -> Bool+hasTyArgs NoTyArgs = False+hasTyArgs SomeTyArgs {} = True++-- | Apply the type arguments to a head type (e.g. a route\/subroute type+-- constructor), left to right. This replaces the ad-hoc+-- @'foldl'' 'AppT' con ('fst' '<$>' tyargs)@ that recurred across the+-- generators.+--+-- @since 1.7.0.0+applyTyArgs :: Type -> TyArgs -> Type+applyTyArgs t = foldl' AppT t . tyArgsTypes+ data ResourceTree typ = ResourceLeaf (Resource typ)- | ResourceParent String CheckOverlap [Piece typ] [ResourceTree typ]- deriving (Show, Functor)+ | ResourceParent String CheckOverlap (Set String) [Piece typ] [ResourceTree typ]+ deriving (Lift, Show, Functor, Foldable, Traversable) resourceTreePieces :: ResourceTree typ -> [Piece typ] resourceTreePieces (ResourceLeaf r) = resourcePieces r-resourceTreePieces (ResourceParent _ _ x _) = x+resourceTreePieces (ResourceParent _ _ _ x _) = x resourceTreeName :: ResourceTree typ -> String resourceTreeName (ResourceLeaf r) = resourceName r-resourceTreeName (ResourceParent x _ _ _) = x--instance Lift t => Lift (ResourceTree t) where- lift (ResourceLeaf r) = [|ResourceLeaf $(lift r)|]- lift (ResourceParent a b c d) = [|ResourceParent $(lift a) $(lift b) $(lift c) $(lift d)|]+resourceTreeName (ResourceParent x _ _ _ _) = x data Resource typ = Resource { resourceName :: String@@ -42,23 +120,12 @@ , resourceAttrs :: [String] , resourceCheck :: CheckOverlap }- deriving (Show, Functor)+ deriving (Lift, Show, Functor, Foldable, Traversable) type CheckOverlap = Bool -instance Lift t => Lift (Resource t) where- lift (Resource a b c d e) = [|Resource a b c d e|]- data Piece typ = Static String | Dynamic typ- deriving Show--instance Functor Piece where- fmap _ (Static s) = Static s- fmap f (Dynamic t) = Dynamic (f t)--instance Lift t => Lift (Piece t) where- lift (Static s) = [|Static $(lift s)|]- lift (Dynamic t) = [|Dynamic $(lift t)|]+ deriving (Lift, Show, Functor, Foldable, Traversable) data Dispatch typ = Methods@@ -69,23 +136,25 @@ { subsiteType :: typ , subsiteFunc :: String }- deriving Show--instance Functor Dispatch where- fmap f (Methods a b) = Methods (fmap f a) b- fmap f (Subsite a b) = Subsite (f a) b--instance Lift t => Lift (Dispatch t) where- lift (Methods Nothing b) = [|Methods Nothing $(lift b)|]- lift (Methods (Just t) b) = [|Methods (Just $(lift t)) $(lift b)|]- lift (Subsite t b) = [|Subsite $(lift t) $(lift b)|]+ deriving (Lift, Show, Functor, Foldable, Traversable) resourceMulti :: Resource typ -> Maybe typ resourceMulti Resource { resourceDispatch = Methods (Just t) _ } = Just t resourceMulti _ = Nothing +-- | The details of a 'ResourceParent' gathered for a flattened resource: the+-- parent's name, its path pieces, and the route attributes it contributes to+-- its descendants.+--+-- @since 1.7.0.0+data ParentDetails a = ParentDetails+ { pdName :: String+ , pdPieces :: [Piece a]+ , pdAttrs :: Set String+ } deriving (Show)+ data FlatResource a = FlatResource- { frParentPieces :: [(String, [Piece a])]+ { frParentDetails :: [ParentDetails a] , frName :: String , frPieces :: [Piece a] , frDispatch :: Dispatch a@@ -96,6 +165,7 @@ flatten = concatMap (go id True) where- go front check' (ResourceLeaf (Resource a b c _ check)) = [FlatResource (front []) a b c (check' && check)]- go front check' (ResourceParent name check pieces children) =- concatMap (go (front . ((name, pieces):)) (check && check')) children+ go front check' (ResourceLeaf (Resource a b c _ check)) =+ [FlatResource (front []) a b c (check' && check)]+ go front check' (ResourceParent name check attrs pieces children) =+ concatMap (go (front . ((ParentDetails name pieces attrs):)) (check && check')) children
test/Hierarchy.hs view
@@ -1,184 +1,109 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE CPP #-}++{-# OPTIONS_GHC -Wno-orphans #-}++-- | Whole-tree end of the @Hierarchy@ split-codegen fixture: generates the+-- full @RenderRoute@\/@RouteAttrs@\/@ParseRoute@\/@YesodDispatch@ instances for+-- the shared tree (from "Hierarchy.ResourceTree") in one set of splices, and+-- supplies every leaf handler. The @Hierarchy.*@ sibling modules instead each+-- emit one focused nested fragment; importing them here checks the per-fragment+-- and whole-tree codegen agree. Drives the @test-routes@ suite's @hierarchy@ spec. module Hierarchy ( hierarchy- , Dispatcher (..)- , runHandler- , Handler2- , App- , toText- , Env (..)- , subDispatch -- to avoid warnings , deleteDelete2 , deleteDelete3+ , testRouteDatatype ) where +import Yesod.Core import Test.Hspec import Test.HUnit-import Yesod.Routes.Parse import Yesod.Routes.TH import Yesod.Routes.Class import Language.Haskell.TH.Syntax-import Data.Text (Text, pack, unpack, append)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as S8+import Data.Text (Text) import qualified Data.Set as Set+import Hierarchy.Admin+import Hierarchy.ResourceTree+import Hierarchy.Nest+import Hierarchy.Nest2+import Hierarchy.Nest3+import Hierarchy.Nest2.NestInner -class ToText a where- toText :: a -> Text+-- | Trivial handler synonym kept only for the @deleteDelete2@\/@deleteDelete3@+-- placeholder handlers below (which exist solely to silence+-- not-in-scope warnings for routes whose handlers are never invoked here).+type Handler site a = a -instance ToText Text where toText = id-instance ToText String where toText = pack+do+ fmap concat $ sequence+ [ mkRenderRouteInstanceOpts defaultOpts [] NoTyArgs (ConT ''Hierarchy) hierarchyResourcesWithType+ , pure <$> mkRouteAttrsInstance [] (ConT ''Hierarchy) hierarchyResourcesWithType+ , mkParseRouteInstance NoTyArgs [] (ConT ''Hierarchy) hierarchyResourcesWithType+ , mkYesodDispatchOpts defaultOpts "Hierarchy" hierarchyResources+ ] -type Handler2 sub master a = a-type Handler site a = Handler2 site site a+instance Yesod Hierarchy -type Request = ([Text], ByteString) -- path info, method-type App sub master = Request -> (Text, Maybe (Route master))-data Env sub master = Env- { envToMaster :: Route sub -> Route master- , envSub :: sub- , envMaster :: master- } -subDispatch- :: (Env sub master -> App sub master)- -> (Handler2 sub master Text -> Env sub master -> Maybe (Route sub) -> App sub master)- -> (master -> sub)- -> (Route sub -> Route master)- -> Env master master- -> App sub master-subDispatch handler _runHandler getSub toMaster env req =- handler env' req- where- env' = env- { envToMaster = envToMaster env . toMaster- , envSub = getSub $ envMaster env- }--class Dispatcher sub master where- dispatcher :: Env sub master -> App sub master+getAfter :: HandlerFor site Text+getAfter = pure "after"+getHomeR :: HandlerFor site Text+getHomeR = pure "homer" -runHandler- :: ToText a- => Handler2 sub master a- -> Env sub master- -> Maybe (Route sub)- -> App sub master-runHandler h Env {..} route _ = (toText h, fmap envToMaster route)+getBackwardsR :: Int -> HandlerFor site Text+getBackwardsR _i = pure "backwards" -data Hierarchy = Hierarchy+getGet3 :: HandlerFor site Text+getGet3 = pure "getget3" -do- let resources = [parseRoutes|-/ HomeR GET+postPost3 :: HandlerFor site Text+postPost3 = pure "postPost3" -----------------------------------------+getNestInnerIndexR :: HandlerFor site Text+getNestInnerIndexR = pure "getNestInnerIndexR" -/!#Int BackwardsR GET+getGetPostR :: HandlerFor site Text+getGetPostR = pure "getGetPostR" -/admin/#Int AdminR:- / AdminRootR GET- /login LoginR GET POST- /table/#Text TableR GET+postGetPostR :: HandlerFor site Text+postGetPostR = pure "postGetPostR" -/nest/ NestR !NestingAttr:+getGet2 :: HandlerFor site Text+getGet2 = pure "getGet2" - /spaces SpacedR GET !NonNested+postPost2 :: HandlerFor site Text+postPost2 = pure "postPost2" - /nest2 Nest2:- / GetPostR GET POST- /get Get2 GET- /post Post2 POST--- /#Int Delete2 DELETE- /nest3 Nest3:- /get Get3 GET- /post Post3 POST--- /#Int Delete3 DELETE+getSpacedR :: HandlerFor site Text+getSpacedR = pure "getSpacedR" -/afterwards AfterR !parent !key=value1:- / After GET !child !key=value2+getAdminRootR :: Int -> HandlerFor site Text+getAdminRootR _ = pure "getAdminRootR" --- /trailing-nest TrailingNestR:--- /foo TrailingFooR GET--- /#Int TrailingIntR GET-|]+getLoginR :: Int -> HandlerFor site Text+getLoginR _ = pure "getLoginR" - rrinst <- mkRenderRouteInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources- rainst <- mkRouteAttrsInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources- prinst <- mkParseRouteInstance [] (ConT ''Hierarchy) $ map (fmap parseType) resources- dispatch <- mkDispatchClause MkDispatchSettings- { mdsRunHandler = [|runHandler|]- , mdsSubDispatcher = [|subDispatch|]- , mdsGetPathInfo = [|fst|]- , mdsMethod = [|snd|]- , mdsSetPathInfo = [|\p (_, m) -> (p, m)|]- , mds404 = [|pack "404"|]- , mds405 = [|pack "405"|]- , mdsGetHandler = defaultGetHandler- , mdsUnwrapper = return- } resources- return-#if MIN_VERSION_template_haskell(2,11,0)- $ InstanceD Nothing-#else- $ InstanceD-#endif- []- (ConT ''Dispatcher- `AppT` ConT ''Hierarchy- `AppT` ConT ''Hierarchy)- [FunD (mkName "dispatcher") [dispatch]]- : prinst- : rainst- : rrinst+postLoginR :: Int -> HandlerFor site Text+postLoginR _ = pure "postLoginR" -getSpacedR :: Handler site String-getSpacedR = "root-leaf"+getTableR :: Int -> Text -> HandlerFor site Text+getTableR _ _ = pure "getTableR" -getGet2 :: Handler site String; getGet2 = "get"-postPost2 :: Handler site String; postPost2 = "post" deleteDelete2 :: Int -> Handler site String; deleteDelete2 = const "delete"-getGet3 :: Handler site String; getGet3 = "get"-postPost3 :: Handler site String; postPost3 = "post" deleteDelete3 :: Int -> Handler site String; deleteDelete3 = const "delete" -getAfter :: Handler site String; getAfter = "after"--getHomeR :: Handler site String-getHomeR = "home"--getBackwardsR :: Int -> Handler site Text-getBackwardsR _ = pack "backwards"--getAdminRootR :: Int -> Handler site Text-getAdminRootR i = pack $ "admin root: " ++ show i--getLoginR :: Int -> Handler site Text-getLoginR i = pack $ "login: " ++ show i--postLoginR :: Int -> Handler site Text-postLoginR i = pack $ "post login: " ++ show i--getTableR :: Int -> Text -> Handler site Text-getTableR _ = append "TableR "--getGetPostR :: Handler site Text-getGetPostR = pack "get"--postGetPostR :: Handler site Text-postGetPostR = pack "post"-- hierarchy :: Spec hierarchy = describe "hierarchy" $ do it "nested with spacing" $@@ -187,33 +112,92 @@ renderRoute (AdminR 5 AdminRootR) @?= (["admin", "5"], []) it "renders table correctly" $ renderRoute (AdminR 6 $ TableR "foo") @?= (["admin", "6", "table", "foo"], [])- let disp m ps = dispatcher- (Env- { envToMaster = id- , envMaster = Hierarchy- , envSub = Hierarchy- })- (map pack ps, S8.pack m)+ describe "parseRoute" $ do+ let parseNothing = Nothing :: Maybe (Route Hierarchy)+ describe "HomeR" $ do+ it "works" $ do+ parseRoute ([], []) @?= Just HomeR+ it "with extraneous query params" $ do+ parseRoute ([], [("foo", "bar")]) @?= Just HomeR+ describe "AdminR" $ do+ it "works" $ do+ parseRoute (["admin", "5"], []) @?= Just (AdminR 5 AdminRootR)+ it "fails with extra character" $ do+ parseRoute (["admin!", "5"], []) @?= parseNothing+ it "works with a subroute with param" $ do+ parseRoute (["admin", "6", "table", "hello"], []) @?= Just (AdminR 6 (TableR "hello"))+ describe "parseNestedRoute" $ do+ it "works" $ do+ parseRouteNested ([], []) @?= Just AdminRootR+ it "works with param" $ do+ parseRouteNested (["table", "hello"], []) @?= Just (TableR "hello")+ describe "NestR" $ do+ it "fails because there's no top-level handler" $ do+ parseRoute (["nest"], []) @?= parseNothing+ it "works for SpacedR" $ do+ parseRoute (["nest", "spaces"], []) @?= Just (NestR SpacedR)+ it "works with parseRouteNested" $ do+ parseRouteNested (["spaces"], []) @?= Just SpacedR+ describe "Nest2" $ do+ it "works" $ do+ parseRoute (["nest", "nest2"], []) @?= Just (NestR (Nest2 GetPostR))+ describe "NestInner" $ do+ it "works" $ do+ parseRoute (["nest", "nest2", "nest-inner"], []) @?= Just (NestR (Nest2 (NestInner NestInnerIndexR)))+ describe "parseRouteNested" $ do+ describe "NestInner" $ do+ it "works" $ do+ parseRouteNested ([], []) @?= Just NestInnerIndexR+ describe "routeAttrs" $ do+ it "inherited attributes" $ do+ routeAttrs (NestR SpacedR) @?= Set.fromList ["NestingAttr", "NonNested"]+ it "pair attributes" $+ routeAttrs (AfterR After) @?= Set.fromList ["parent", "child", "key=value2"] - let testGetPost route getRes postRes = do- let routeStrs = map unpack $ fst (renderRoute route)- disp "GET" routeStrs @?= (getRes, Just route)- disp "POST" routeStrs @?= (postRes, Just route)+ describe "NestingAttr inherits properly" $ do+ it "routeAttrs" $ do+ routeAttrs (NestR (Nest2 GetPostR)) @?= Set.fromList ["NestingAttr"]+ it "routeAttrsNested" $ do+ routeAttrsNested GetPostR @?= Set.fromList ["NestingAttr"]+ it "routeAttrsNested InnerNest" $ do+ routeAttrsNested NestInnerIndexR @?= Set.fromList ["NestingAttr"] - it "dispatches routes with multiple METHODs: admin" $- testGetPost (AdminR 1 LoginR) "login: 1" "post login: 1" - it "dispatches routes with multiple METHODs: nesting" $- testGetPost (NestR $ Nest2 GetPostR) "get" "post"+-- This value should compile if all routes are present as expected.+testRouteDatatype :: Route Hierarchy -> Int+testRouteDatatype r =+ case r of+ HomeR -> 0+ BackwardsR _ -> 1+ AdminR _ sub ->+ case sub of+ AdminRootR -> 0+ LoginR -> 0+ TableR _ -> 1+ NestR sub -> testNestR sub+ -- NOTE: This is a bug in the behavior of the parser. See issue+ -- https://github.com/yesodweb/yesod/issues/1886+ --+ -- Nest3, by layout, should be under `NestR`. However, since there+ -- is a comment on column 0, this causes the parser to reset the+ -- column count.+ Nest3 sub ->+ case sub of+ Get3 -> 0+ Post3 -> 0+ AfterR sub ->+ case sub of+ After -> 0 - it "dispatches root correctly" $ disp "GET" ["admin", "7"] @?= ("admin root: 7", Just $ AdminR 7 AdminRootR)- it "dispatches table correctly" $ disp "GET" ["admin", "8", "table", "bar"] @?= ("TableR bar", Just $ AdminR 8 $ TableR "bar")- it "parses" $ do- parseRoute ([], []) @?= Just HomeR- parseRoute ([], [("foo", "bar")]) @?= Just HomeR- parseRoute (["admin", "5"], []) @?= Just (AdminR 5 AdminRootR)- parseRoute (["admin!", "5"], []) @?= (Nothing :: Maybe (Route Hierarchy))- it "inherited attributes" $ do- routeAttrs (NestR SpacedR) @?= Set.fromList ["NestingAttr", "NonNested"]- it "pair attributes" $- routeAttrs (AfterR After) @?= Set.fromList ["parent", "child", "key=value2"]+testNestR :: NestR -> Int+testNestR sub =+ case sub of+ SpacedR -> 1+ Nest2 sub' ->+ case sub' of+ GetPostR -> 0+ Get2 -> 0+ Post2 -> 0+ NestInner sub'' ->+ case sub'' of+ NestInnerIndexR -> 0
+ test/Hierarchy/Admin.hs view
@@ -0,0 +1,29 @@+{-# language TemplateHaskell, ViewPatterns, OverloadedStrings, TypeFamilies #-}++-- | @Hierarchy@ split fragment focused on the @AdminR@ parent. Shows the+-- finer-grained helpers — @mkRenderRouteInstanceOpts@ /+-- @mkRouteAttrsInstanceFor@ / @mkParseRouteInstanceFor@ each scoped to+-- @"AdminR"@ — emitting that one fragment's instances against the shared+-- @hierarchyResourcesWithType@ tree, rather than the whole-tree splice in+-- "Hierarchy".+module Hierarchy.Admin where++import Yesod.Routes.TH+import Hierarchy.ResourceTree+import Language.Haskell.TH+import Data.Text (Text)++mkRenderRouteInstanceOpts+ (setFocusOnNestedRoute "AdminR" defaultOpts)+ []+ NoTyArgs+ (ConT ''Hierarchy)+ hierarchyResourcesWithType+mkRouteAttrsInstanceFor+ []+ (ConT ''AdminR)+ "AdminR"+ $ hierarchyResourcesWithType+mkParseRouteInstanceFor+ "AdminR"+ $ hierarchyResourcesWithType
+ test/Hierarchy/Nest.hs view
@@ -0,0 +1,29 @@+{-# language TemplateHaskell #-}+{-# language ViewPatterns #-}+{-# language OverloadedStrings #-}+{-# language TypeFamilies #-}++-- | @Hierarchy@ split fragment focused on the top-level nested parent @NestR@+-- (which itself contains @Nest2@\/@Nest3@). Uses the canonical+-- @mkYesodDataOpts@ with @setFocusOnNestedRoute "NestR"@ to emit just that+-- fragment's route datatype + render\/attrs\/parse instances — the same+-- higher-level entry point a full Yesod site uses to split route compilation+-- across modules (see @yesod-core\/docs\/split-route-compilation.md@). Imports+-- the deeper @Nest2@\/@Nest3@\/@NestInner@ fragment modules so their+-- separately-emitted instances are in scope for @NestR@ to delegate to.+--+-- (Sibling fragments "Hierarchy.Admin", "Hierarchy.Nest2" and+-- "Hierarchy.Nest2.NestInner" instead drive the underlying+-- @mkRenderRouteInstanceOpts@ \/ @mkRouteAttrsInstanceFor@ \/+-- @mkParseRouteInstanceFor@ builders directly, so the route-level generators+-- that @mkYesodDataOpts@ bundles stay covered too.)+module Hierarchy.Nest where++import Yesod.Routes.TH+import Hierarchy.ResourceTree+import Hierarchy.Nest3 ()+import Hierarchy.Nest2+import Hierarchy.Nest2.NestInner+import Yesod.Core++mkYesodDataOpts (setFocusOnNestedRoute "NestR" defaultOpts) "Hierarchy" hierarchyResources
+ test/Hierarchy/Nest2.hs view
@@ -0,0 +1,28 @@+{-# language TemplateHaskell, OverloadedStrings, ViewPatterns, TypeFamilies #-}++-- | @Hierarchy@ split fragment focused on the @Nest2@ parent. Emits @Nest2@'s+-- render\/attrs\/parse instances against the shared @hierarchyResourcesWithType@.+-- The top-of-module @do@ block is a compile-time assertion: it calls+-- @parseRouteNested@ for the deeper @NestInner@ fragment (whose instance is+-- emitted in "Hierarchy.Nest2.NestInner") and @fail@s the splice if parsing the+-- empty path does not yield @NestInnerIndexR@ — pinning cross-module nested parse.+module Hierarchy.Nest2 where++import Yesod.Routes.Class+import Yesod.Routes.TH+import Hierarchy.ResourceTree+import Language.Haskell.TH+import Hierarchy.Nest3 ()+import Hierarchy.Nest2.NestInner++do+ let works = parseRouteNested ([], []) :: Maybe NestInner+ case works of+ Nothing ->+ fail "parsing fails"+ Just NestInnerIndexR ->+ pure []++mkRenderRouteInstanceOpts (setFocusOnNestedRoute "Nest2" defaultOpts) [] NoTyArgs (ConT ''Hierarchy) hierarchyResourcesWithType+mkRouteAttrsInstanceFor [] (ConT ''Nest2) "Nest2" $ hierarchyResourcesWithType+mkParseRouteInstanceFor "Nest2" $ hierarchyResourcesWithType
+ test/Hierarchy/Nest2/NestInner.hs view
@@ -0,0 +1,23 @@+{-# language TemplateHaskell #-}+{-# language ViewPatterns, OverloadedStrings #-}+{-# language TypeFamilies #-}++-- | @Hierarchy@ split fragment focused on the deepest nested parent+-- @NestInner@ (under @NestR@ > @Nest2@). Emits its render\/attrs\/parse+-- instances via @setFocusOnNestedRoute "NestInner"@ + the @*For@ helpers; the+-- @ParseRouteNested NestInner@ instance is what "Hierarchy.Nest2" asserts+-- against at compile time.+module Hierarchy.Nest2.NestInner where++import Yesod.Routes.TH+import Hierarchy.ResourceTree+import Language.Haskell.TH++mkRenderRouteInstanceOpts+ (setFocusOnNestedRoute "NestInner" defaultOpts)+ []+ NoTyArgs+ (ConT ''Hierarchy)+ hierarchyResourcesWithType+mkRouteAttrsInstanceFor [] (ConT ''NestInner) "NestInner" $ hierarchyResourcesWithType+mkParseRouteInstanceFor "NestInner" $ hierarchyResourcesWithType
+ test/Hierarchy/Nest3.hs view
@@ -0,0 +1,16 @@+{-# language TemplateHaskell #-}+{-# language ViewPatterns #-}+{-# language OverloadedStrings #-}+{-# language TypeFamilies #-}++-- | @Hierarchy@ split fragment focused on the @Nest3@ parent. Demonstrates+-- @mkYesodDataOpts@ with @setFocusOnNestedRoute "Nest3"@ emitting just that+-- nested fragment's route datatype + instances, separate from the whole-tree+-- splice in "Hierarchy". Shares @hierarchyResources@ from "Hierarchy.ResourceTree".+module Hierarchy.Nest3 where++import Yesod.Routes.TH+import Hierarchy.ResourceTree+import Yesod.Core++mkYesodDataOpts (setFocusOnNestedRoute "Nest3" defaultOpts) "Hierarchy" hierarchyResources
+ test/Hierarchy/ResourceTree.hs view
@@ -0,0 +1,54 @@+{-# language TemplateHaskell #-}+{-# language QuasiQuotes #-}++-- | Shared resource tree + foundation type for the @Hierarchy@ split-codegen+-- fixture. The @Hierarchy.*@ sibling modules each focus on one nested parent+-- (NestR, Nest2, Nest3, NestInner, AdminR) via @setFocusOnNestedRoute@ and emit+-- that fragment's instances separately, while "Hierarchy" itself emits the+-- whole-tree instances; keeping the tree here lets every splice share it+-- without tripping the TH stage restriction. Used by the @test-routes@ suite.+module Hierarchy.ResourceTree where++import Yesod.Routes.Parse+import Language.Haskell.TH (Type)+import Yesod.Routes.TH++data Hierarchy = Hierarchy++hierarchyResources :: [ResourceTree String]+hierarchyResources = [parseRoutes|+/ HomeR GET++----------------------------------------++/!#Int BackwardsR GET++/admin/#Int AdminR:+ / AdminRootR GET+ /login LoginR GET POST+ /table/#Text TableR GET++/nest/ NestR !NestingAttr:++ /spaces SpacedR GET !NonNested++ /nest2 Nest2:+ / GetPostR GET POST+ /get Get2 GET+ /post Post2 POST+ /nest-inner NestInner:+ / NestInnerIndexR GET+-- lol++ /nest3 Nest3:+ /get Get3 GET+ /post Post3 POST+-- /#Int Delete3 DELETE++/afterwards AfterR !parent !key=value1:+ / After GET !child !key=value2++|]++hierarchyResourcesWithType :: [ResourceTree Type]+hierarchyResourcesWithType = map (fmap parseType) hierarchyResources
+ test/Route/DeepAritySpec.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Exercises the reify/'typeArity' path that 'assertNestedSubArity' runs+-- inside 'mkNestedDispatchInstanceWith'\'s recursion — the part the pure+-- 'checkNestedSubArity' unit tests in "Route.SubDispatchAritySpec" can't reach.+-- A 2nd-level-or-deeper nested datatype whose arity doesn't match the site's+-- type-argument count must be caught by the guard's @fail@ (an actionable+-- 'ArityMismatch') rather than slip through to a cryptic kind error in+-- generated code.+module Route.DeepAritySpec (spec) where++import Test.Hspec+import Language.Haskell.TH (recover)+import Yesod.Routes.TH.Internal+ ( assertNestedSubArity+ , resolveRouteCon+ , typeArity+ , ArityCallSite(..)+ , SubsiteName(..)+ , SubsiteArity(..)+ )+import Route.DeepArityTypes (ParamSite, MatchingDeepR, WrongDeepR)++-- | The reified arities feeding the guard, extracted at compile time through+-- the same 'typeArity' call 'assertNestedSubArity' makes.+reifiedArities :: (Int, Int, Int)+reifiedArities =+ $(do+ a <- typeArity ''ParamSite+ b <- typeArity ''MatchingDeepR+ c <- typeArity ''WrongDeepR+ [| (a, b, c) |])++-- | 'True' iff the arity guard fired its @fail@ for a (site arity 1) vs.+-- (@WrongDeepR@ arity 0) pairing — i.e. the deep mismatch was caught at the+-- guard rather than escaping into ill-kinded generated code.+wrongArityCaught :: Bool+wrongArityCaught =+ $(do+ rc <- resolveRouteCon "WrongDeepR"+ recover [| True |] $ do+ assertNestedSubArity TopLevelCall (SubsiteName "ParamSite") (SubsiteArity 1) rc+ [| False |])++-- | 'True' iff a matching (arity 1 vs. 1) pairing passes the guard cleanly.+matchingArityOk :: Bool+matchingArityOk =+ $(do+ rc <- resolveRouteCon "MatchingDeepR"+ recover [| False |] $ do+ assertNestedSubArity TopLevelCall (SubsiteName "ParamSite") (SubsiteArity 1) rc+ [| True |])++spec :: Spec+spec = describe "deep nested-route arity guard (reify path)" $ do+ it "reifies datatype arities used by the guard" $+ reifiedArities `shouldBe` (1, 1, 0)+ it "catches a deep arity mismatch with the actionable guard (not a kind error)" $+ wrongArityCaught `shouldBe` True+ it "lets a correctly-parameterized deep nested datatype through" $+ matchingArityOk `shouldBe` True
+ test/Route/DeepArityTypes.hs view
@@ -0,0 +1,15 @@+-- | Datatypes for "Route.DeepAritySpec", in their own module so the spec's+-- compile-time splices can 'reify' them — a datatype defined in the same module+-- as a top-level splice is not yet in the type environment at a 'reify'.+module Route.DeepArityTypes+ ( ParamSite (..)+ , MatchingDeepR (..)+ , WrongDeepR (..)+ ) where++-- A parameterized site (arity 1) plus two candidate nested datatypes: one whose+-- arity matches (1) and one whose arity is wrong (0). These stand in for a+-- deeply-nested subroute reached by the dispatch recursion.+data ParamSite a = ParamSite+data MatchingDeepR a = MatchingDeepR+data WrongDeepR = WrongDeepR
+ test/Route/DiscoveryModeSpec.hs view
@@ -0,0 +1,49 @@+-- | Unit tests for the 'discoveryMode' classifier. It is a pure, total+-- function but previously had no direct coverage; every behavioural test only+-- exercised it indirectly through generated code.+module Route.DiscoveryModeSpec (spec) where++import Test.Hspec+import Yesod.Routes.TH.RenderRoute+ ( defaultOpts+ , setParameterizedSubroute+ , setFocusOnNestedRoute+ , DiscoveryMode (..)+ , discoveryMode+ )+import Yesod.Routes.TH.Types (TyArgs, toTyArgs)+import Language.Haskell.TH (Type (ConT), mkName)++-- | A site with no type parameters ('hasTyArgs' 'False').+noArgs :: TyArgs+noArgs = toTyArgs []++-- | A site with one type parameter ('hasTyArgs' 'True').+someArgs :: TyArgs+someArgs = toTyArgs [(ConT (mkName "Int"), mkName "a")]++spec :: Spec+spec = describe "discoveryMode" $ do+ -- Dimension 1: no type args (a monomorphic site is always nested).+ it "is NestedDiscovery for a monomorphic site, even with all opts off" $+ discoveryMode defaultOpts noArgs `shouldBe` NestedDiscovery++ -- Dimensions 2/3 off, has type args: the backwards-compatible inline case.+ it "is InlineCompat for a parameterized site with default opts" $+ discoveryMode defaultOpts someArgs `shouldBe` InlineCompat++ -- Dimension 2: roParameterizedSubroute in the parameterized case.+ it "is NestedDiscovery for a parameterized site when parameterized-subroute is set" $+ discoveryMode (setParameterizedSubroute True defaultOpts) someArgs+ `shouldBe` NestedDiscovery++ -- Dimension 3: roFocusOnNestedRoute in the parameterized case.+ it "is NestedDiscovery for a parameterized site when focusing on a nested route" $+ discoveryMode (setFocusOnNestedRoute "Target" defaultOpts) someArgs+ `shouldBe` NestedDiscovery++ it "stays NestedDiscovery for a monomorphic site with the flags set" $ do+ discoveryMode (setParameterizedSubroute True defaultOpts) noArgs+ `shouldBe` NestedDiscovery+ discoveryMode (setFocusOnNestedRoute "Target" defaultOpts) noArgs+ `shouldBe` NestedDiscovery
+ test/Route/FallthroughSpec.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE ViewPatterns#-}+++-- | The nested-route fallthrough contract for a top-level site, generated with+-- @setNestedRouteFallthrough True@. Two same-prefix parents share @\/foo@+-- (@FirstFooR@ index-only, @SecondFooR@ with deeper children). With fallthrough+-- on, a request missing inside @FirstFooR@ continues to the sibling rather than+-- committing. Asserts at both layers: @parseRoute@ falls through to the right+-- constructor, and a WAI dispatch round-trip (via 'YesodCoreTest.RuntimeHarness')+-- serves the expected handler.+module Route.FallthroughSpec where++import Network.Wai+import Yesod.Core+import Test.Hspec+import Data.ByteString.Lazy (ByteString)++import YesodCoreTest.RuntimeHarness (assertRequestRaw)++data App = App++do+ let resources = [parseRoutesNoCheck|++/ HomeR+/foo FirstFooR:+ / FooIndexR+/foo SecondFooR:+ /blah FooBlahR+ /baz FooBaz1R:+ / FooBazIndexR+ /baz FooBaz2R:+ /foo FooBaz2FooR++ |]++ let opts = setNestedRouteFallthrough True defaultOpts+ mkYesodOpts opts "App" resources++instance Yesod App where+ messageLoggerSource = mempty++handleFooBlahR :: HandlerFor site String+handleFooBlahR = pure "FooBlahR"++handleHomeR :: HandlerFor site String+handleHomeR = pure "HomeR"++handleFooBaz2FooR :: HandlerFor site String+handleFooBaz2FooR = pure "FooBaz2FooR"++handleFooBazIndexR :: HandlerFor site String+handleFooBazIndexR = pure "FooBazIndexR"++handleFooIndexR :: HandlerFor site String+handleFooIndexR = pure "FooIndexR"++spec :: Spec+spec = do+ describe "parseRoute" $ do+ let routeShouldParse path result =+ parseRoute (path, []) `shouldBe` Just result+ routeShouldNotParse path =+ parseRoute (path, []) `shouldBe` (Nothing :: Maybe (Route App))+ it "matches the first parent without falling through" $ do+ routeShouldParse ["foo"] (FirstFooR FooIndexR)+ it "can fall through" $ do+ routeShouldParse ["foo", "blah"] (SecondFooR FooBlahR)+ it "nested fallthrough works too" $ do+ routeShouldParse ["foo", "baz", "foo"] (SecondFooR (FooBaz2R FooBaz2FooR))+ it "can fail" $ do+ routeShouldNotParse ["asdf"]++ describe "Dispatch" $ do+ it "/" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = []+ }+ (Just "HomeR")+ it "/foo" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["foo"]+ }+ (Just "FooIndexR")++testRequestIO :: HasCallStack => Int -- ^ http status code+ -> Request+ -> Maybe ByteString -- ^ expected body+ -> IO ()+testRequestIO status req mexpected =+ assertRequestRaw (toWaiApp App) req status mexpected
+ test/Route/FocusLeafConsSpec.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE TemplateHaskell #-}+-- | This module pins the constructor set of the generated @FocusNestR@+-- datatype exactly, so escalate an incomplete 'describeFocus' to an error.+{-# OPTIONS_GHC -Werror=incomplete-patterns #-}++-- | Regression test for 'mkRouteConsOpts' in focus (module-split) mode.+--+-- Previously the focused branch dropped every 'ResourceLeaf' among the+-- target's children (returning @([], [])@ for leaves), so the focused+-- datatype was generated without its leaf constructors. Here we splice a+-- datatype built directly from 'mkRouteConsOpts'' focused output, then assert+-- its constructor set is /exactly/ what we expect via a total 'case':+--+-- * a /missing/ constructor fails the pattern reference (a name that isn't+-- in scope), and+-- * an /extra/ constructor makes 'describeFocus' non-exhaustive, which+-- @-Werror=incomplete-patterns@ (set above) turns into a compile error.+--+-- So this module fails to build unless @FocusNestR@ has precisely the two leaf+-- constructors @FocusIndexR@ and @FocusShowR Int@.+module Route.FocusLeafConsSpec (spec) where++import Test.Hspec+import Language.Haskell.TH+import Yesod.Routes.TH.Types+import Yesod.Routes.TH.RenderRoute+ ( mkRouteConsOpts+ , defaultOpts+ , setFocusOnNestedRoute+ )++data App = App++-- | A nested route @FocusNestR@ whose children are two leaves. We build the+-- focused datatype from 'mkRouteConsOpts' and declare it by hand from the+-- returned constructors.+$(do+ let leaf name pieces =+ ResourceLeaf (Resource name pieces (Methods Nothing ["GET"]) [] True)+ parent name pieces =+ ResourceParent name True mempty pieces+ routes =+ [ leaf "FocusHomeR" []+ , parent "FocusNestR" []+ [ leaf "FocusIndexR" []+ , leaf "FocusShowR" [Dynamic (ConT ''Int)]+ ]+ ]+ (cons, _decs) <-+ mkRouteConsOpts+ (setFocusOnNestedRoute "FocusNestR" defaultOpts)+ []+ NoTyArgs+ (ConT ''App)+ routes+ pure [DataD [] (mkName "FocusNestR") [] Nothing cons [DerivClause Nothing [ConT ''Show]]])++-- | Total over every constructor of the generated @FocusNestR@. Naming each+-- constructor exactly once is what makes this a complete-set assertion: see+-- the module header for how missing/extra constructors are each rejected at+-- compile time.+describeFocus :: FocusNestR -> String+describeFocus r = case r of+ FocusIndexR -> "FocusIndexR"+ FocusShowR n -> "FocusShowR " ++ show n++spec :: Spec+spec = describe "mkRouteConsOpts (focus mode)" $+ it "generates exactly the focused route's leaf constructors" $ do+ -- The real assertion is at compile time (see 'describeFocus'); these+ -- just exercise the generated constructors.+ describeFocus FocusIndexR `shouldBe` "FocusIndexR"+ describeFocus (FocusShowR 7) `shouldBe` "FocusShowR 7"
+ test/Route/InlineParseClausesSpec.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskellQuotes #-}++-- | Unit tests for 'buildInlineParseClauses', the pure (effect-free) core of+-- the backwards-compatible inline @parseRoute@ codegen. Because it is pure —+-- it takes a deterministic fresh-name supply instead of 'Q'\'s 'newName' and+-- builds AST directly rather than through quotation brackets — its '[Clause]'+-- output can be asserted on directly, with no splicing, compilation, or+-- 'runQ'. This is the testability win from extracting the pure builder out of+-- 'generateParseRouteClausesInline'.+module Route.InlineParseClausesSpec (spec) where++import Test.Hspec+import Data.Maybe (catMaybes)+import Language.Haskell.TH+import Web.PathPieces (fromPathPiece)+import Yesod.Routes.TH.Types+import Yesod.Routes.TH.ParseRoute (buildInlineParseClauses)+import Route.PureQ (runPureQ)++-- | Run the pure builder at the top level (no wrapping), the way+-- 'generateParseRouteClausesInline' invokes it.+run :: ResourceTree Type -> [Clause]+run t = runPureQ (buildInlineParseClauses id t)++leaf :: String -> [Piece Type] -> Dispatch Type -> ResourceTree Type+leaf name pieces d = ResourceLeaf (Resource name pieces d [] True)++methods :: [String] -> Dispatch Type+methods = Methods Nothing++parent :: String -> [Piece Type] -> [ResourceTree Type] -> ResourceTree Type+parent name pieces = ResourceParent name True mempty pieces++-- | Assert there is exactly one clause and hand it to the continuation.+withOnlyClause :: [Clause] -> (Clause -> Expectation) -> Expectation+withOnlyClause cs k =+ case cs of+ [c] -> k c+ _ -> expectationFailure $ "expected exactly one clause, got " <> show (length cs)++-- | Variables bound by a @fromPathPiece@ view pattern, descending through the+-- list\/tuple\/constructor patterns the inline builder nests them in.+boundDynVars :: Pat -> [Name]+boundDynVars (ViewP (VarE f) inner)+ | f == 'fromPathPiece = simpleVars inner+boundDynVars p = concatMap boundDynVars (subPats p)++-- | Plain 'VarP' names anywhere within a pattern.+simpleVars :: Pat -> [Name]+simpleVars (VarP n) = [n]+simpleVars p = concatMap simpleVars (subPats p)++-- | The immediate sub-patterns of a pattern (only the shapes the inline+-- builder produces need handling).+subPats :: Pat -> [Pat]+subPats (TupP ps) = ps+subPats (ListP ps) = ps+subPats (ViewP _ p) = [p]+subPats (ParensP p) = [p]+subPats (BangP p) = [p]+subPats (TildeP p) = [p]+subPats (AsP _ p) = [p]+subPats (SigP p _) = [p]+#if MIN_VERSION_template_haskell(2,18,0)+subPats (ConP _ _ ps) = ps+#else+subPats (ConP _ ps) = ps+#endif+subPats _ = []++-- | The variables referenced in an expression.+usedVars :: Exp -> [Name]+usedVars (VarE n) = [n]+usedVars e = concatMap usedVars (subExps e)++-- | The match alternatives of the top-level expression, when it is a @case@+-- (the shape a parent's committing clause produces). 'Nothing' otherwise.+caseMatches :: Exp -> Maybe [Match]+caseMatches (CaseE _ ms) = Just ms+caseMatches _ = Nothing++-- | The body expression of a clause that has no @where@ bindings.+clauseExp :: Clause -> Exp+clauseExp (Clause _ body _) = clauseBodyExp body++-- | Whether a match alternative is the catch-all @_ -> Nothing@ fallback.+isNothingFallback :: Match -> Bool+isNothingFallback (Match WildP (NormalB (ConE n)) []) = n == 'Nothing+isNothingFallback _ = False++-- | The immediate sub-expressions of an expression.+subExps :: Exp -> [Exp]+subExps (AppE a b) = [a, b]+subExps (AppTypeE e _) = [e]+subExps (InfixE ma _ mb) = catMaybes [ma, mb]+subExps (UInfixE a b c) = [a, b, c]+subExps (ParensE e) = [e]+subExps (LamE _ e) = [e]+subExps (SigE e _) = [e]+subExps (CondE a b c) = [a, b, c]+subExps (CaseE e ms) = e : [me | Match _ (NormalB me) _ <- ms]+subExps (ListE es) = es+#if MIN_VERSION_template_haskell(2,16,0)+subExps (TupE mes) = catMaybes mes+#else+subExps (TupE es) = es+#endif+subExps _ = []++-- | The expression in a clause body (the inline builder emits 'NormalB' for+-- leaves and a single-guard 'GuardedB' under fallthrough).+clauseBodyExp :: Body -> Exp+clauseBodyExp (NormalB e) = e+clauseBodyExp (GuardedB ((_, e) : _)) = e+-- Total on the (unreached) empty-guard case: a var-free sentinel so a stray+-- input fails an assertion locally instead of aborting the whole spec run.+clauseBodyExp (GuardedB []) = ConE '()++spec :: Spec+spec = describe "buildInlineParseClauses (pure inline parseRoute codegen)" $ do+ it "produces one clause for a single static leaf" $+ length (run (leaf "HomeR" [Static "home"] (methods ["GET"]))) `shouldBe` 1++ it "emits exactly one (committing) clause per parent, matching its prefix once" $ do+ -- 1.6 semantics: the parent contributes ONE top-level clause that+ -- matches the parent prefix, then cases over its children. It does not+ -- expand to one flat clause per descendant leaf (which would re-parse+ -- the prefix and let a child miss fall through to a sibling route).+ let tree = parent "AdminR" [Static "admin"]+ [ leaf "UsersR" [Static "users"] (methods ["GET"])+ , leaf "PostsR" [Static "posts"] (methods ["GET"])+ ]+ withOnlyClause (run tree) $ \c ->+ case caseMatches (clauseExp c) of+ Nothing -> expectationFailure "expected the parent clause body to be a case over children"+ -- two children + the committing Nothing fallback+ Just ms -> length ms `shouldBe` 3++ it "ends the parent's case with a Nothing fallback (commit-on-parent-prefix)" $ do+ -- This is the heart of the backwards-compat fix: once the parent prefix+ -- matches, a child miss must resolve to 'Nothing' (a 404 that agrees+ -- with dispatch), NOT fall through to a later top-level route.+ let tree = parent "AdminR" [Static "admin"]+ [ leaf "UsersR" [Static "users"] (methods ["GET"]) ]+ withOnlyClause (run tree) $ \c ->+ case caseMatches (clauseExp c) of+ Nothing -> expectationFailure "expected the parent clause body to be a case over children"+ Just ms -> case reverse ms of+ (lastM : _) -> isNothingFallback lastM `shouldBe` True+ [] -> expectationFailure "expected at least the Nothing fallback"++ it "commits at each level for deeply-nested parents (nested cases, each Nothing-terminated)" $ do+ let tree = parent "AR" [Static "a"]+ [ parent "BR" [Static "b"]+ [ leaf "CR" [Static "c"] (methods ["GET"])+ , leaf "DR" [Static "d"] (methods ["GET"])+ ]+ ]+ -- One top-level clause for AR; its case has one child match (BR's+ -- committing clause-as-match) plus the Nothing fallback.+ withOnlyClause (run tree) $ \c ->+ case caseMatches (clauseExp c) of+ Nothing -> expectationFailure "expected AR's body to be a case"+ Just msA -> do+ length msA `shouldBe` 2+ isNothingFallback (last msA) `shouldBe` True+ -- The non-fallback alternative is BR's committing match,+ -- whose body is itself a case (over CR/DR) ending in Nothing.+ case msA of+ (Match _ (NormalB innerE) [] : _) ->+ case caseMatches innerE of+ Nothing -> expectationFailure "expected BR's body to be a case"+ Just msB -> do+ length msB `shouldBe` 3 -- CR, DR, Nothing+ isNothingFallback (last msB) `shouldBe` True+ _ -> expectationFailure "unexpected shape for BR's match alternative"++ it "binds a dynamic piece with a fromPathPiece view pattern" $+ withOnlyClause (run (leaf "UserR" [Static "user", Dynamic (ConT ''Int)] (methods ["GET"]))) $ \c ->+ pprint c `shouldContain` "fromPathPiece"++ it "uses fromPathMultiPiece for a multipiece leaf" $+ withOnlyClause (run (leaf "FilesR" [Static "files"] (Methods (Just (ConT ''String)) ["GET"]))) $ \c ->+ pprint c `shouldContain` "fromPathMultiPiece"++ it "delegates a subsite leaf through parseRoute" $+ withOnlyClause (run (leaf "SubR" [Static "sub"] (Subsite (ConT (mkName "SubSite")) "getSub"))) $ \c ->+ pprint c `shouldContain` "parseRoute"++ it "prefixes accumulated parent pieces onto each descendant clause" $ do+ -- The parent's static piece must appear in the (single) child clause,+ -- alongside the child's own piece — that is what \"inline\" means.+ let tree = parent "OrgR" [Static "org", Dynamic (ConT ''Int)]+ [ leaf "TeamR" [Static "team"] (methods ["GET"]) ]+ withOnlyClause (run tree) $ \c -> do+ pprint c `shouldContain` "org"+ pprint c `shouldContain` "team"++ it "threads the parent's dynamic binder into the reconstructed route body" $ do+ -- A parent with a dynamic piece: its binder must be matched in the path+ -- pattern (via 'fromPathPiece') *and* fed back into the reconstructed+ -- route in the body. That threading — not merely that the pieces show+ -- up textually — is the property the inline arm has to preserve.+ let tree = parent "OrgR" [Static "org", Dynamic (ConT ''Int)]+ [ leaf "TeamR" [Static "team"] (methods ["GET"]) ]+ withOnlyClause (run tree) $ \(Clause pats body _) -> do+ let dynVars = concatMap boundDynVars pats+ used = usedVars (clauseBodyExp body)+ -- Exactly one dynamic piece (the parent's, bound via fromPathPiece)+ -- and it is referenced in the reconstructed body — i.e. one 'True'.+ -- Empty dynVars gives @[] /= [True]@, so this never passes vacuously.+ map (`elem` used) dynVars `shouldBe` [True]
+ test/Route/InstanceProbeSpec.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Pins the answer to: /can 'isInstance' fail if the route datatype's+-- type parameters remain abstract?/ ('nestedInstanceExists'+-- saturates a resolved datatype with fresh, unannotated 'VarT's via+-- 'fullyApplyType' rather than ground types.)+--+-- The exercised contract:+--+-- * Saturation to the datatype's /own/ reified arity keeps the probed head+-- well-kinded, so the probe is a total query — \"could any instance+-- match\" — that returns 'True'\/'False' but never aborts the splice.+-- * It holds even when a parameter is higher-kinded (@(f :: Type -> Type)@):+-- the bare @VarT@ 'fullyApplyType' supplies has its kind inferred from the+-- datatype's declared kind, so the head is still well-kinded.+-- * The arity-0 case (no parameters) degenerates to probing the bare+-- constructor.+--+-- Each probe runs inside a compile-time splice wrapped in 'recover': if+-- 'isInstance' ever threw (the failure mode guarded against here), the+-- splice would fall through to the 'recover' handler, which 'error's, and the+-- expectation would fail loudly rather than silently.+module Route.InstanceProbeSpec (spec) where++import Test.Hspec+import Language.Haskell.TH (recover, isInstance, Type (ConT, AppT))+import Yesod.Routes.TH.Internal (nestedInstanceExists, resolveRouteCon)+import Route.InstanceProbeTypes+ ( Probe, HasInst, HasInst2, HasInstInt, NoInst, HK, HKInst, Mono+ , ProbePoly, PolyFull, PolyUnapplied, PolyPartial+ )++spec :: Spec+spec = describe "nestedInstanceExists / fullyApplyType abstract-parameter probe" $ do+ it "returns True for an arity-1 datatype that has an instance" $+ $(do+ rc <- resolveRouteCon "HasInst"+ recover [| error "isInstance crashed on HasInst" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` True++ it "returns True for an arity-2 datatype with an instance at abstract parameters" $+ $(do+ rc <- resolveRouteCon "HasInst2"+ recover [| error "isInstance crashed on HasInst2" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` True++ -- Whether probing the abstract head @HasInstInt a@ finds the concrete+ -- @instance Probe (HasInstInt Int)@ depends on the GHC version: GHC 9.0++ -- 'reifyInstances' returns unifiers (so a bare type variable in the query+ -- unifies with @Int@ and the probe reports 'True'), whereas GHC < 9.0 does+ -- not return the concrete-argument instance for a type-variable query head+ -- (so it reports 'False'). This concrete-argument shape never arises in real+ -- codegen — nested-discovery instances are always emitted at fully-abstract+ -- parameters (the 'HasInst' case above), which every supported GHC resolves+ -- identically — so this case only pins the observed 'reifyInstances'+ -- divergence, not a behaviour the codegen relies on.+#if __GLASGOW_HASKELL__ >= 900+ it "returns True when the only instance is at a concrete argument (a unifier counts as could-match)" $+ $(do+ rc <- resolveRouteCon "HasInstInt"+ recover [| error "isInstance crashed on HasInstInt" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` True+#else+ it "returns False when the only instance is at a concrete argument (GHC < 9.0 reifyInstances does not unify a type-variable query head)" $+ $(do+ rc <- resolveRouteCon "HasInstInt"+ recover [| error "isInstance crashed on HasInstInt" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` False+#endif++ it "returns False for an arity-1 datatype with no instance" $+ $(do+ rc <- resolveRouteCon "NoInst"+ recover [| error "isInstance crashed on NoInst" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` False++ it "does not crash on a higher-kinded (f :: Type -> Type) parameter with no instance (returns False)" $+ $(do+ rc <- resolveRouteCon "HK"+ recover [| error "isInstance crashed on HK" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` False++ it "does not crash on a higher-kinded parameter with an instance (returns True)" $+ $(do+ rc <- resolveRouteCon "HKInst"+ recover [| error "isInstance crashed on HKInst" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` True++ it "handles the zero-arity case (bare constructor head) with an instance" $+ $(do+ rc <- resolveRouteCon "Mono"+ recover [| error "isInstance crashed on Mono" |] $ do+ b <- nestedInstanceExists ''Probe rc+ [| b |]) `shouldBe` True++ -- Poly-kinded class: lets us represent (and so probe) instances whose head+ -- is not fully applied — the shapes a 'Type'-kinded class rejects outright.+ it "ProbePoly: returns True for an instance at the fully-applied head" $+ $(do+ rc <- resolveRouteCon "PolyFull"+ recover [| error "isInstance crashed on PolyFull" |] $ do+ b <- nestedInstanceExists ''ProbePoly rc+ [| b |]) `shouldBe` True++ it "ProbePoly: returns False when the only instance is at the unapplied constructor (the arity-saturated probe head doesn't match it)" $+ $(do+ rc <- resolveRouteCon "PolyUnapplied"+ recover [| error "isInstance crashed on PolyUnapplied" |] $ do+ b <- nestedInstanceExists ''ProbePoly rc+ [| b |]) `shouldBe` False++ it "ProbePoly: returns False when the only instance is at a partially-applied head" $+ $(do+ rc <- resolveRouteCon "PolyPartial"+ recover [| error "isInstance crashed on PolyPartial" |] $ do+ b <- nestedInstanceExists ''ProbePoly rc+ [| b |]) `shouldBe` False++ -- Why 'nestedInstanceExists' must saturate to the datatype's *full* arity:+ -- the partial instance @ProbePoly (PolyPartial a)@ matches *any* one-argument+ -- application of @PolyPartial@. Probing one argument short — at @PolyPartial+ -- Int@ (kind Type -> Type) — therefore matches it, while the full-arity head+ -- @PolyPartial a b@ (kind Type, what the probe actually uses) does not. So a+ -- probe that stopped short would be spuriously fooled by a partial instance;+ -- saturating fully is what avoids that. These pin that matching boundary by+ -- calling 'isInstance' directly at hand-built heads.+ it "isInstance matches the partial instance one argument short (PolyPartial Int)" $+ $(do+ recover [| error "isInstance crashed on PolyPartial Int" |] $ do+ b <- isInstance ''ProbePoly [ConT ''PolyPartial `AppT` ConT ''Int]+ [| b |]) `shouldBe` True++ it "isInstance does not match the partial instance at the full application (PolyPartial Int Int)" $+ $(do+ recover [| error "isInstance crashed on PolyPartial Int Int" |] $ do+ b <- isInstance ''ProbePoly [ConT ''PolyPartial `AppT` ConT ''Int `AppT` ConT ''Int]+ [| b |]) `shouldBe` False++ it "isInstance does not match the unapplied-constructor instance once an argument is applied (PolyUnapplied Int)" $+ $(do+ recover [| error "isInstance crashed on PolyUnapplied Int" |] $ do+ b <- isInstance ''ProbePoly [ConT ''PolyUnapplied `AppT` ConT ''Int]+ [| b |]) `shouldBe` False
+ test/Route/InstanceProbeTypes.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE PolyKinds #-}++-- | Datatypes and a stand-in probe class for "Route.InstanceProbeSpec", in+-- their own module so the spec's compile-time splices can 'reify' them — a+-- datatype defined in the same module as a top-level splice is not yet in the+-- type environment at a 'reify' (the same reason "Route.DeepArityTypes" is+-- separate).+--+-- 'Probe' stands in for any of the real nested-discovery classes+-- (@YesodDispatchNested@, @RenderRouteNested@, …) that 'nestedInstanceExists'+-- probes: those classes aren't visible to the pure @test-routes@ suite, but+-- 'nestedInstanceExists' only ever asks @isInstance klass [head]@, so any+-- single-parameter class with the right instance shape exercises the same+-- code path.+module Route.InstanceProbeTypes+ ( Probe+ , HasInst (..)+ , HasInst2 (..)+ , HasInstInt (..)+ , NoInst (..)+ , HK (..)+ , HKInst (..)+ , Mono (..)+ -- * Poly-kinded probe (under-applied instance heads)+ , ProbePoly+ , PolyFull (..)+ , PolyUnapplied (..)+ , PolyPartial (..)+ ) where++import Data.Kind (Type)++-- | A single-parameter probe class, standing in for the nested-discovery+-- classes 'nestedInstanceExists' checks against.+class Probe a++-- | Arity-1, kind-'Type' parameter, /with/ a @Probe@ instance — the probe must+-- answer 'True'. The instance is given at a fully abstract parameter, exactly+-- as our codegen emits per-route-datatype instances.+data HasInst a = HasInst+instance Probe (HasInst a)++-- | Arity-2: 'fullyApplyType' must saturate with /two/ fresh 'VarT's before+-- the head is well-kinded. With an instance at fully abstract parameters the+-- probe answers 'True'.+data HasInst2 a b = HasInst2+instance Probe (HasInst2 a b)++-- | Arity-1 whose only instance is at a /concrete/ argument. The probe asks+-- \"could any instance match\". On GHC 9.0+ 'reifyInstances' returns unifiers+-- (not just exact matches), so probing the abstract @HasInstInt a@ finds the+-- @Int@ instance and answers 'True'; on GHC < 9.0 'reifyInstances' does not+-- unify a bare type-variable query head against the concrete instance, so it+-- answers 'False'. (Real codegen never emits instances at concrete arguments,+-- so this divergence does not affect nested discovery; see the matching test in+-- "Route.InstanceProbeSpec".)+--+-- (Instances at an /under-applied/ constructor — the bare @instance Probe+-- HasInstInt@, or an arity-2 datatype applied to one argument like+-- @instance Probe (HasInst2 a)@ — can't be written against this 'Type'-kinded+-- 'Probe': a head of kind @Type -> Type@ is kind-rejected at its definition+-- site (GHC: \"Expecting one more argument to …; Expected a type, but … has+-- kind @* -> *@\"). To probe those shapes at all they must be made+-- representable by a /poly-kinded/ class; see 'ProbePoly' below.)+data HasInstInt a = HasInstInt+instance Probe (HasInstInt Int)++-- | Arity-1, kind-'Type' parameter, /without/ a @Probe@ instance — the probe+-- must answer 'False'.+data NoInst a = NoInst++-- | Arity-1 with a kind-annotated, non-'Type' parameter @(f :: Type -> Type)@+-- and /no/ instance. The point of this case: 'fullyApplyType' saturates @HK@+-- with a bare, unannotated @VarT@ whose kind GHC must infer from @HK@'s+-- declared kind. The probe must not crash and must report 'False'.+data HK (f :: Type -> Type) = HK++-- | As 'HK', but /with/ an instance given at an abstract higher-kinded+-- parameter. The probe must report 'True' without crashing.+data HKInst (f :: Type -> Type) = HKInst+instance Probe (HKInst f)++-- | Arity-0 (the zero type-parameter case): 'fullyApplyType' applies no+-- arguments, so the probed head is the bare constructor. With an instance the+-- probe answers 'True'.+data Mono = Mono+instance Probe Mono++-- | A /poly-kinded/ probe class. Unlike 'Probe' (kind @Type -> Constraint@),+-- @ProbePoly@ accepts a head of any kind, which makes the under-applied+-- instance shapes that 'Probe' rejects at their definition site representable —+-- so we can pin down what 'nestedInstanceExists' does when the only instance is+-- at such a head. The probe always saturates the datatype to its /own/ arity+-- (a kind-'Type' head; see 'fullyApplyType'), so these cases also confirm it+-- queries at the fully-applied head specifically.+class ProbePoly (a :: k)++-- | Poly-kinded class, instance at the /fully-applied/ head (kind 'Type').+-- Positive control: the probe saturates @PolyFull@ to @PolyFull a@ and matches.+data PolyFull a = PolyFull+instance ProbePoly (PolyFull a)++-- | Instance at the /unapplied/ arity-1 constructor (head kind @Type -> Type@),+-- representable only because 'ProbePoly' is poly-kinded. The probe saturates to+-- @PolyUnapplied a@ before querying, so the instance at the bare constructor is+-- a different head.+data PolyUnapplied a = PolyUnapplied+instance ProbePoly PolyUnapplied++-- | Instance at a /partially-applied/ arity-2 constructor (head kind+-- @Type -> Type@). As 'PolyUnapplied', the full-arity probe head+-- @PolyPartial a b@ is a different head from the partial instance @PolyPartial a@.+data PolyPartial a b = PolyPartial+instance ProbePoly (PolyPartial a)
+ test/Route/MissingFocusTargetSpec.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Exercises the TH failure paths that fire when a 'setFocusOnNestedRoute'+-- target does not exist in the resource tree. Historically the 'ParseRoute'+-- and 'RouteAttrs' generators silently produced an always-'Nothing' \/+-- always-'mempty' nested instance for a bogus target, while the 'Dispatch' and+-- 'RenderRoute' generators correctly 'fail'ed. All four are now routed through+-- the shared 'findNestedRoute' lookup and 'fail' with the same message.+--+-- A 'Q' 'fail' raises its message through 'qReport' rather than carrying it in+-- the thrown value (so 'runQ' just reports @\"Q monad failure\"@), so — as in+-- "Route.DeepAritySpec" — we assert at compile time with 'recover': each+-- generator is run inside a splice against a nonexistent target and must take+-- its @fail@ path (driving 'recover' to its handler). The shared lookup that+-- produces the \"was not found in resources\" message is unit-tested directly+-- via 'findNestedRoute'.+module Route.MissingFocusTargetSpec (spec) where++import Test.Hspec+import Data.Maybe (isNothing)+import Language.Haskell.TH (recover)++import Yesod.Routes.TH.Types (resourceTreeName, TyArgs(NoTyArgs))+import Yesod.Routes.TH.ParseRoute (mkParseRouteInstanceFor)+import Yesod.Routes.TH.RouteAttrs (mkRouteAttrsInstanceFor)+import Yesod.Routes.TH.RenderRoute+ (mkRenderRouteInstanceOpts, defaultOpts, setFocusOnNestedRoute)+import Yesod.Routes.TH.Dispatch (mkDispatchInstance)+import Yesod.Routes.TH.Internal (findNestedRoute)+import Route.MissingFocusTargetTypes (sampleTree, bogusType, bogusName)++-- | 'True' iff the named generator 'fail'ed on the bogus focus target (i.e.+-- 'recover' fell through to its @True@ handler); 'False' if it wrongly produced+-- declarations. Each is forced at compile time so a regression to a silent+-- always-empty instance flips the corresponding boolean.++parseRouteFailed :: Bool+parseRouteFailed =+ $(recover [| True |] $ do+ _ <- mkParseRouteInstanceFor bogusName sampleTree+ [| False |])++routeAttrsFailed :: Bool+routeAttrsFailed =+ $(recover [| True |] $ do+ _ <- mkRouteAttrsInstanceFor [] bogusType bogusName sampleTree+ [| False |])++renderRouteFailed :: Bool+renderRouteFailed =+ $(recover [| True |] $ do+ _ <- mkRenderRouteInstanceOpts+ (setFocusOnNestedRoute bogusName defaultOpts)+ [] NoTyArgs bogusType sampleTree+ [| False |])++dispatchFailed :: Bool+dispatchFailed =+ $(recover [| True |] $ do+ _ <- mkDispatchInstance+ (setFocusOnNestedRoute bogusName defaultOpts)+ bogusType [] NoTyArgs pure sampleTree+ [| False |])++spec :: Spec+spec = describe "missing focus target" $ do+ describe "fails every nested generator (recover path)" $ do+ it "ParseRoute (mkParseRouteInstanceFor)" $+ parseRouteFailed `shouldBe` True+ it "RouteAttrs (mkRouteAttrsInstanceFor)" $+ routeAttrsFailed `shouldBe` True+ it "RenderRoute (mkRenderRouteInstanceOpts)" $+ renderRouteFailed `shouldBe` True+ it "Dispatch (mkDispatchInstance)" $+ dispatchFailed `shouldBe` True++ describe "shared lookup (findNestedRoute)" $ do+ it "returns Nothing for a target absent from the tree" $+ isNothing (findNestedRoute bogusName sampleTree)+ `shouldBe` True+ it "returns the subtree for a target present in the tree" $+ (fmap (map resourceTreeName . snd) (findNestedRoute "ParentR" sampleTree))+ `shouldBe` Just ["ChildR"]
+ test/Route/MissingFocusTargetTypes.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Shared fixtures for "Route.MissingFocusTargetSpec". The resource tree and+-- bogus type live here (a separate module) so that the spec can use them inside+-- top-level TH splices without tripping the GHC stage restriction.+module Route.MissingFocusTargetTypes+ ( sampleTree+ , bogusType+ , bogusName+ ) where++import Language.Haskell.TH.Syntax (Type(ConT), mkName)+import Yesod.Routes.TH.Types++-- A tiny tree with a single real nested parent ("ParentR"). The generators+-- under test are never handed that name; they get 'bogusName', which is absent.+sampleTree :: [ResourceTree Type]+sampleTree =+ [ ResourceParent "ParentR" True mempty [Static "parent"]+ [ ResourceLeaf $+ Resource "ChildR" [] (Methods Nothing ["GET"]) ["child"] True+ ]+ ]++bogusName :: String+bogusName = "DefinitelyNotARealRouteR"++bogusType :: Type+bogusType = ConT (mkName bogusName)
+ test/Route/NestedParseClausesSpec.hs view
@@ -0,0 +1,61 @@+-- | Unit tests for 'generateParseRouteClause' run in 'PureQ' — the+-- monad-polymorphic (over 'Quote') nested @parseRouteNested@ clause generator.+-- Production runs it in 'Q' (hygienic 'newName'); here we pin its name supply to+-- a deterministic counter so its 'Clause' output — and the set of parent names+-- it requests a 'ParseRouteNested' instance for — can be asserted directly, with+-- no splicing or 'runQ'. This is the payoff of hoisting the @isInstance@ query+-- out of the generator (see 'generateParseRouteClause').+module Route.NestedParseClausesSpec (spec) where++import Test.Hspec+import Control.Monad.State.Strict (runStateT)+import qualified Data.Set as Set+import Data.Set (Set)+import Language.Haskell.TH+import Yesod.Routes.TH.Types+import Yesod.Routes.TH.RenderRoute (defaultOpts)+import Yesod.Routes.TH.ParseRoute (generateParseRouteClause)+import Route.PureQ (runPureQ)++-- | Run the nested clause generator in 'PureQ': deterministic fresh names, pure+-- output. Returns the clause plus the set of parent names it requested a+-- 'ParseRouteNested' instance for.+run :: Set String -> ResourceTree Type -> (Clause, Set String)+run existing t =+ runPureQ $+ runStateT (generateParseRouteClause existing defaultOpts t) mempty++leaf :: String -> [Piece Type] -> Dispatch Type -> ResourceTree Type+leaf name pieces d = ResourceLeaf (Resource name pieces d [] True)++methods :: [String] -> Dispatch Type+methods = Methods Nothing++parent :: String -> [Piece Type] -> [ResourceTree Type] -> ResourceTree Type+parent name pieces = ResourceParent name True mempty pieces++fooParent :: ResourceTree Type+fooParent =+ parent "FooR" [Static "foo"]+ [ leaf "BarR" [Static "bar"] (methods ["GET"]) ]++spec :: Spec+spec = describe "generateParseRouteClause (nested parseRoute codegen in PureQ)" $ do+ it "records a parent that has no nested instance yet" $+ snd (run mempty fooParent) `shouldBe` Set.singleton "FooR"++ it "does not record a parent that already has a nested instance (hoisted query)" $+ snd (run (Set.singleton "FooR") fooParent) `shouldBe` mempty++ it "delegates a parent clause through parseRouteNested" $+ pprint (fst (run mempty fooParent)) `shouldContain` "parseRouteNested"++ it "records nothing for a bare leaf" $+ snd (run mempty (leaf "BarR" [Static "bar"] (methods ["GET"]))) `shouldBe` mempty++ it "is reproducible: re-running the same tree yields an identical clause (names and all)" $+ -- The PureQ counter resets to 0 each run, so the deterministic binders+ -- make the whole Clause '=='. Under Q's 'newName' the uniques would+ -- differ between runs — this equality is exactly what going through the+ -- pure 'Quote' instance buys the tests.+ run mempty fooParent `shouldBe` run mempty fooParent
+ test/Route/PureQ.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- | A pure 'Quote' instance for unit-testing the TH clause builders without+-- 'Q'. The builders are monad-polymorphic over 'Quote' (their only effect is+-- fresh-'Name' generation): production runs them in 'Q' for hygienic 'newName'+-- binders, while these tests run them in 'PureQ', where 'newName' is a+-- deterministic monotonic counter. That makes the generated '[Clause]' AST+-- fully determined and directly comparable — no splicing or 'runQ' needed.+--+-- The deterministic names use non-hygienic 'mkName', which is fine for+-- pure-inspection tests but would risk shadowing in real splices — hence+-- production stays on 'Q' and only the tests pin the builder's monad to 'PureQ'.+module Route.PureQ+ ( PureQ+ , runPureQ+ ) where++import Control.Monad.State.Strict (State, evalState, state)+import Language.Haskell.TH.Syntax (Name, mkName)+import Language.Haskell.TH.Syntax.Compat (Quote(..))++-- | A pure name supply: 'State' over a monotonic counter.+newtype PureQ a = PureQ (State Int a)+ deriving (Functor, Applicative, Monad)++-- | Run a 'PureQ' computation, starting the counter at @0@. Re-running the same+-- builder therefore yields an identical AST (binders and all).+runPureQ :: PureQ a -> a+runPureQ (PureQ s) = evalState s 0++-- | Each 'newName' yields a distinct 'mkName' from the counter, so the builder's+-- output is reproducible and comparable.+instance Quote PureQ where+ newName base = PureQ $ state $ \n -> (mkName (base ++ show n), n + 1)
+ test/Route/RenderRouteSpec.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE ViewPatterns#-}++-- | Behavioural spec for @renderRouteNested@ on a deeply-nested route tree.+-- Pins the contract that rendering a nested leaf reconstructs the /entire/ path+-- from the site root (parent pieces and captures included), not just the leaf's+-- own fragment — the caller supplies the ancestor captures as a tuple.+module Route.RenderRouteSpec where++import Yesod.Core+import Test.Hspec++data App = App++mkYesodData "App" [parseRoutes|++/foo/#Int FooR:+ / FooIndexR+ /bar/#String FooBarR:+ / FooBarIndexR+ /baz FooBarBazR:+ /new FooBarBazNewR+ /delete/#String FooBarBazDeleteR+ /get/#String FooBarBazGetR++ |]++spec :: Spec+spec = do+ describe "renderRouteNested" $ do+ it "renders the entire route, not just end fragment" $ do+ renderRouteNested (3, "hello") (FooBarBazGetR "asdf")+ `shouldBe`+ (["foo", "3", "bar", "hello", "baz", "get", "asdf"], [])
+ test/Route/RouteAttrSpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE QuasiQuotes #-}++-- | Pure spec asserting that route attributes (@!x@) declared on a /nested+-- parent/ are captured in its flattened 'frParentDetails' (@pdAttrs@), not just+-- on leaves. Operates directly on the parsed 'ResourceTree' decision data — no+-- splicing or codegen — so it pins where in the flattened structure parent+-- attrs land.+module Route.RouteAttrSpec (spec) where++import Yesod.Core+import Test.Hspec+import Yesod.Routes.TH.Types+import Data.Set (Set)+import qualified Data.Set as Set++routeNoAttributes :: [ResourceTree String]+routeNoAttributes = [parseRoutes|+/one OneR:+ /two TwoR:+ /three ThreeR:+ /four FourR POST+ |]++routeWithAttributes :: [ResourceTree String]+routeWithAttributes = [parseRoutes|+/one OneR:+ /two TwoR !x !z:+ /three ThreeR !y:+ /four FourR POST+ |]++spec :: Spec+spec = do+ describe "route attrs present" $ do+ it "has no route attrs on parent" $ do+ let parentDetails = concat $ frParentDetails <$> flatten routeNoAttributes+ let attrs = (\detail -> (pdName detail, pdAttrs detail)) <$> parentDetails+ Set.fromList attrs+ `shouldBe`+ Set.fromList + [ ("OneR", Set.empty)+ , ("TwoR", Set.empty)+ , ("ThreeR", Set.empty)+ ]+ it "has route attrs on parent" $ do+ let parentDetails = concat $ frParentDetails <$> flatten routeWithAttributes+ let attrs = (\detail -> (pdName detail, pdAttrs detail)) <$> parentDetails+ Set.fromList attrs+ `shouldBe`+ Set.fromList + [ ("OneR", Set.empty)+ , ("TwoR", Set.fromList ["x", "z"])+ , ("ThreeR", Set.singleton "y")+ ]
+ test/Route/SubDispatchAritySpec.hs view
@@ -0,0 +1,78 @@+-- | Unit tests for 'checkNestedSubArity', the guard that gives+-- 'mkYesodSubDispatchInstance' an actionable error (instead of a cryptic kind+-- error) when a parameterized subsite is paired with unparameterized nested+-- route datatypes.+module Route.SubDispatchAritySpec (spec) where++import Test.Hspec+import Yesod.Routes.TH.Internal+ ( checkNestedSubArity+ , arityMismatchMessage+ , ArityCallSite(..)+ , ArityMismatch(..)+ , SubsiteName(..)+ , RouteName(..)+ , SubsiteArity(..)+ , RouteArity(..)+ )++-- | The newtype wrappers are positional noise in the tests; this names the+-- call the way the production caller does. 'Nothing' means the arities match;+-- 'Just' carries the rendered mismatch message (phrased for a 'SubsiteCall').+check :: String -> String -> Int -> Int -> Maybe String+check sub route subArgs routeArity =+ fmap (arityMismatchMessage SubsiteCall) $+ checkNestedSubArity+ (SubsiteName sub)+ (RouteName route)+ (SubsiteArity subArgs)+ (RouteArity routeArity)++spec :: Spec+spec = describe "checkNestedSubArity" $ do+ it "accepts a monomorphic subsite with an unparameterized nested datatype" $+ check "MySub" "NestedR" 0 0 `shouldBe` Nothing++ it "accepts a parameterized subsite whose nested datatype carries the param" $+ check "MySub" "NestedR" 1 1 `shouldBe` Nothing++ it "rejects when the nested datatype carries more params than the subsite" $+ -- Over-arity leaves the instance head partially applied (kind+ -- @Type -> ...@), so this must be rejected, not silently accepted.+ case check "MySub" "NestedR" 1 2 of+ Nothing -> expectationFailure "expected an arity mismatch"+ Just msg -> do+ msg `shouldContain` "`NestedR` has 2 type parameter(s)"+ msg `shouldContain` "the subsite `MySub` has 1"++ it "rejects a parameterized subsite with an unparameterized nested datatype" $+ check "MySub" "NestedR" 1 0 `shouldBe` Just+ (concat+ [ "mkYesodSubDispatchInstance: the nested route datatype "+ , "`NestedR` has 0 type parameter(s), but the subsite `MySub` "+ , "has 1. The subroute datatypes must carry exactly the "+ , "subsite's type parameter(s) \8212 generate the subsite's "+ , "routes with `mkYesodSubDataOpts (setParameterizedSubroute "+ , "True defaultOpts) ...` so a parameterized subsite gets "+ , "parameterized subroutes."+ ])++ it "names both types in the error message" $+ case check "MySub" "NestedR" 1 0 of+ Nothing -> expectationFailure "expected a mismatch"+ Just msg -> do+ msg `shouldContain` "MySub"+ msg `shouldContain` "NestedR"++ describe "arityMismatchMessage phrasing per call site" $ do+ let mismatch = ArityMismatch (SubsiteName "MySub") (RouteName "NestedR")+ (SubsiteArity 1) (RouteArity 0)+ it "names mkYesodSubDispatchInstance and calls it a subsite for a SubsiteCall" $ do+ let msg = arityMismatchMessage SubsiteCall mismatch+ msg `shouldContain` "mkYesodSubDispatchInstance"+ msg `shouldContain` "subsite"+ it "names mkYesod and calls it a site (not a subsite) for a TopLevelCall" $ do+ let msg = arityMismatchMessage TopLevelCall mismatch+ msg `shouldContain` "mkYesod:"+ msg `shouldNotContain` "subsite"+ msg `shouldContain` "site"
test/RouteSpec.hs view
@@ -1,29 +1,46 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE ViewPatterns#-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS_GHC -ddump-splices #-}+{-# LANGUAGE ViewPatterns#-}++-- | Entry point ('main') for the @test-routes@ suite — the pure-routing tests+-- that need no running site. Aggregates the @Route.*@ specs (fallthrough,+-- render, attrs, the pure decision/codegen specs, arity guards, focus-target+-- lookups, ...) and the @Hierarchy@ fixture, then adds inline RenderRoute /+-- route-parsing / overlap-checking / @parseRouteType@ checks below.+ import Test.Hspec import Test.HUnit ((@?=))-import Data.Text (Text, pack, unpack, singleton)+import Data.Text (Text) import Yesod.Routes.Class hiding (Route)-import qualified Yesod.Routes.Class as YRC-import Yesod.Routes.Parse (parseRoutesFile, parseRoutesNoCheck, parseTypeTree, TypeTree (..))+import Yesod.Routes.Parse (parseTypeTree, TypeTree (..))+import Yesod.Core import Yesod.Routes.Overlap (findOverlapNames) import Yesod.Routes.TH hiding (Dispatch)-import Language.Haskell.TH.Syntax import Hierarchy-import qualified Data.ByteString.Char8 as S8 import qualified Data.Set as Set+import qualified Route.FallthroughSpec as FallthroughSpec+import qualified Route.RenderRouteSpec as RenderRouteSpec+import qualified YesodCoreTest.RenderRouteSpec as NestedRenderRouteSpec+import qualified Route.RouteAttrSpec as RouteAttrSpec+import qualified Route.DiscoveryModeSpec as DiscoveryModeSpec+import qualified Route.SubDispatchAritySpec as SubDispatchAritySpec+import qualified Route.DeepAritySpec as DeepAritySpec+import qualified Route.InstanceProbeSpec as InstanceProbeSpec+import qualified Route.InlineParseClausesSpec as InlineParseClausesSpec+import qualified Route.NestedParseClausesSpec as NestedParseClausesSpec+import qualified Route.FocusLeafConsSpec as FocusLeafConsSpec+import qualified Route.MissingFocusTargetSpec as MissingFocusTargetSpec+import qualified Data.Text as Text data MyApp = MyApp @@ -46,188 +63,77 @@ Route MySubParam = ParamRoute Char deriving (Show, Eq, Read)- renderRoute (ParamRoute x) = ([singleton x], [])+ renderRoute (ParamRoute x) = ([Text.singleton x], []) instance ParseRoute MySubParam where- parseRoute ([unpack -> [x]], _) = Just $ ParamRoute x+ parseRoute ([Text.unpack -> [x]], _) = Just $ ParamRoute x parseRoute _ = Nothing getMySubParam :: MyApp -> Int -> MySubParam getMySubParam _ = MySubParam do- texts <- [t|[Text]|]+ texts <- pure "[Text]" let resLeaves = map ResourceLeaf [ Resource "RootR" [] (Methods Nothing ["GET"]) ["foo", "bar"] True- , Resource "BlogPostR" [Static "blog", Dynamic $ ConT ''Text] (Methods Nothing ["GET", "POST"]) [] True+ , Resource "BlogPostR" [Static "blog", Dynamic "Text"] (Methods Nothing ["GET", "POST"]) [] True , Resource "WikiR" [Static "wiki"] (Methods (Just texts) []) [] True- , Resource "SubsiteR" [Static "subsite"] (Subsite (ConT ''MySub) "getMySub") [] True- , Resource "SubparamR" [Static "subparam", Dynamic $ ConT ''Int] (Subsite (ConT ''MySubParam) "getMySubParam") [] True+ , Resource "SubsiteR" [Static "subsite"] (Subsite "MySub" "getMySub") [] True+ , Resource "SubparamR" [Static "subparam", Dynamic "Int"] (Subsite "MySubParam" "getMySubParam") [] True ] resParent = ResourceParent "ParentR" True+ mempty [ Static "foo"- , Dynamic $ ConT ''Text+ , Dynamic "Text" ] [ ResourceLeaf $ Resource "ChildR" [] (Methods Nothing ["GET"]) ["child"] True ] ress = resParent : resLeaves- rrinst <- mkRenderRouteInstance [] (ConT ''MyApp) ress- rainst <- mkRouteAttrsInstance [] (ConT ''MyApp) ress- prinst <- mkParseRouteInstance [] (ConT ''MyApp) ress- dispatch <- mkDispatchClause MkDispatchSettings- { mdsRunHandler = [|runHandler|]- , mdsSubDispatcher = [|subDispatch dispatcher|]- , mdsGetPathInfo = [|fst|]- , mdsMethod = [|snd|]- , mdsSetPathInfo = [|\p (_, m) -> (p, m)|]- , mds404 = [|pack "404"|]- , mds405 = [|pack "405"|]- , mdsGetHandler = defaultGetHandler- , mdsUnwrapper = return- } ress- return-#if MIN_VERSION_template_haskell(2,11,0)- $ InstanceD Nothing-#else- $ InstanceD-#endif- []- (ConT ''Dispatcher- `AppT` ConT ''MyApp- `AppT` ConT ''MyApp)- [FunD (mkName "dispatcher") [dispatch]]- : prinst- : rainst- : rrinst+ mkYesod "MyApp" ress -instance Dispatcher MySub master where- dispatcher env (pieces, _method) =- ( pack $ "subsite: " ++ show pieces- , Just $ envToMaster env route- )- where- route = MySubRoute (pieces, [])+instance YesodSubDispatch MySub MyApp where+ yesodSubDispatch _yre = undefined -instance Dispatcher MySubParam master where- dispatcher env (pieces, _method) =- case map unpack pieces of- [[c]] ->- let route = ParamRoute c- toMaster = envToMaster env- MySubParam i = envSub env- in ( pack $ "subparam " ++ show i ++ ' ' : [c]- , Just $ toMaster route- )- _ -> (pack "404", Nothing)+instance YesodSubDispatch MySubParam MyApp where+ yesodSubDispatch _yre = undefined -{--thDispatchAlias- :: (master ~ MyApp, sub ~ MyApp, handler ~ String, app ~ (String, Maybe (YRC.Route MyApp)))- => master- -> sub- -> (YRC.Route sub -> YRC.Route master)- -> app -- ^ 404 page- -> handler -- ^ 405 page- -> Text -- ^ method- -> [Text]- -> app---thDispatchAlias = thDispatch-thDispatchAlias master sub toMaster app404 handler405 method0 pieces0 =- case dispatch pieces0 of- Just f -> f master sub toMaster app404 handler405 method0- Nothing -> app404- where- dispatch = toDispatch- [ Route [] False $ \pieces ->- case pieces of- [] -> do- Just $ \master' sub' toMaster' _app404' handler405' method ->- let handler =- case Map.lookup method methodsRootR of- Just f -> f- Nothing -> handler405'- in runHandler handler master' sub' RootR toMaster'- _ -> error "Invariant violated"- , Route [D.Static "blog", D.Dynamic] False $ \pieces ->- case pieces of- [_, x2] -> do- y2 <- fromPathPiece x2- Just $ \master' sub' toMaster' _app404' handler405' method ->- let handler =- case Map.lookup method methodsBlogPostR of- Just f -> f y2- Nothing -> handler405'- in runHandler handler master' sub' (BlogPostR y2) toMaster'- _ -> error "Invariant violated"- , Route [D.Static "wiki"] True $ \pieces ->- case pieces of- _:x2 -> do- y2 <- fromPathMultiPiece x2- Just $ \master' sub' toMaster' _app404' _handler405' _method ->- let handler = handleWikiR y2- in runHandler handler master' sub' (WikiR y2) toMaster'- _ -> error "Invariant violated"- , Route [D.Static "subsite"] True $ \pieces ->- case pieces of- _:x2 -> do- Just $ \master' sub' toMaster' app404' handler405' method ->- dispatcher master' (getMySub sub') (toMaster' . SubsiteR) app404' handler405' method x2- _ -> error "Invariant violated"- , Route [D.Static "subparam", D.Dynamic] True $ \pieces ->- case pieces of- _:x2:x3 -> do- y2 <- fromPathPiece x2- Just $ \master' sub' toMaster' app404' handler405' method ->- dispatcher master' (getMySubParam sub' y2) (toMaster' . SubparamR y2) app404' handler405' method x3- _ -> error "Invariant violated"- ]- methodsRootR = Map.fromList [("GET", getRootR)]- methodsBlogPostR = Map.fromList [("GET", getBlogPostR), ("POST", postBlogPostR)]--}+instance Yesod MyApp where+ messageLoggerSource = mempty main :: IO () main = hspec $ do+ describe "Route.FallthroughSpec" FallthroughSpec.spec+ describe "Route.RenderRouteSpec" RenderRouteSpec.spec+ describe "YesodCoreTest.RenderRouteSpec" NestedRenderRouteSpec.spec+ describe "Route.RouteAttrSpec" RouteAttrSpec.spec+ describe "Route.DiscoveryModeSpec" DiscoveryModeSpec.spec+ describe "Route.SubDispatchAritySpec" SubDispatchAritySpec.spec+ describe "Route.DeepAritySpec" DeepAritySpec.spec+ describe "Route.InstanceProbeSpec" InstanceProbeSpec.spec+ describe "Route.InlineParseClausesSpec" InlineParseClausesSpec.spec+ describe "Route.NestedParseClausesSpec" NestedParseClausesSpec.spec+ describe "Route.FocusLeafConsSpec" FocusLeafConsSpec.spec+ describe "Route.MissingFocusTargetSpec" MissingFocusTargetSpec.spec describe "RenderRoute instance" $ do it "renders root correctly" $ renderRoute RootR @?= ([], [])- it "renders blog post correctly" $ renderRoute (BlogPostR $ pack "foo") @?= (map pack ["blog", "foo"], [])- it "renders wiki correctly" $ renderRoute (WikiR $ map pack ["foo", "bar"]) @?= (map pack ["wiki", "foo", "bar"], [])- it "renders subsite correctly" $ renderRoute (SubsiteR $ MySubRoute (map pack ["foo", "bar"], [(pack "baz", pack "bin")]))- @?= (map pack ["subsite", "foo", "bar"], [(pack "baz", pack "bin")])+ it "renders blog post correctly" $ renderRoute (BlogPostR $ Text.pack "foo") @?= (map Text.pack ["blog", "foo"], [])+ it "renders wiki correctly" $ renderRoute (WikiR $ map Text.pack ["foo", "bar"]) @?= (map Text.pack ["wiki", "foo", "bar"], [])+ it "renders subsite correctly" $ renderRoute (SubsiteR $ MySubRoute (map Text.pack ["foo", "bar"], [(Text.pack "baz", Text.pack "bin")]))+ @?= (map Text.pack ["subsite", "foo", "bar"], [(Text.pack "baz", Text.pack "bin")]) it "renders subsite param correctly" $ renderRoute (SubparamR 6 $ ParamRoute 'c')- @?= (map pack ["subparam", "6", "c"], [])+ @?= (map Text.pack ["subparam", "6", "c"], []) - describe "thDispatch" $ do- let disp m ps = dispatcher- (Env- { envToMaster = id- , envMaster = MyApp- , envSub = MyApp- })- (map pack ps, S8.pack m)- it "routes to root" $ disp "GET" [] @?= (pack "this is the root", Just RootR)- it "POST root is 405" $ disp "POST" [] @?= (pack "405", Just RootR)- it "invalid page is a 404" $ disp "GET" ["not-found"] @?= (pack "404", Nothing :: Maybe (YRC.Route MyApp))- it "routes to blog post" $ disp "GET" ["blog", "somepost"]- @?= (pack "some blog post: somepost", Just $ BlogPostR $ pack "somepost")- it "routes to blog post, POST method" $ disp "POST" ["blog", "somepost2"]- @?= (pack "POST some blog post: somepost2", Just $ BlogPostR $ pack "somepost2")- it "routes to wiki" $ disp "DELETE" ["wiki", "foo", "bar"]- @?= (pack "the wiki: [\"foo\",\"bar\"]", Just $ WikiR $ map pack ["foo", "bar"])- it "routes to subsite" $ disp "PUT" ["subsite", "baz"]- @?= (pack "subsite: [\"baz\"]", Just $ SubsiteR $ MySubRoute ([pack "baz"], []))- it "routes to subparam" $ disp "PUT" ["subparam", "6", "q"]- @?= (pack "subparam 6 q", Just $ SubparamR 6 $ ParamRoute 'q') describe "route parsing" $ do it "subsites work" $ do- parseRoute ([pack "subsite", pack "foo"], [(pack "bar", pack "baz")]) @?=- Just (SubsiteR $ MySubRoute ([pack "foo"], [(pack "bar", pack "baz")]))+ parseRoute ([Text.pack "subsite", Text.pack "foo"], [(Text.pack "bar", Text.pack "baz")]) @?=+ Just (SubsiteR $ MySubRoute ([Text.pack "foo"], [(Text.pack "bar", Text.pack "baz")])) describe "routing table parsing" $ do it "recognizes trailing backslashes as line continuation directives" $ do let routes :: [ResourceTree String]- routes = $(parseRoutesFile "test/fixtures/routes_with_line_continuations")+ routes = $(parseRoutesFile "test/fixtures/routes_with_line_continuations.yesodroutes") length routes @?= 3 describe "overlap checking" $ do@@ -316,9 +222,9 @@ findOverlapNames routes @?= [] describe "routeAttrs" $ do it "works" $ do- routeAttrs RootR @?= Set.fromList [pack "foo", pack "bar"]+ routeAttrs RootR @?= Set.fromList [Text.pack "foo", Text.pack "bar"] it "hierarchy" $ do- routeAttrs (ParentR (pack "ignored") ChildR) @?= Set.singleton (pack "child")+ routeAttrs (ParentR (Text.pack "ignored") ChildR) @?= Set.singleton (Text.pack "child") hierarchy describe "parseRouteType" $ do let success s t = it s $ parseTypeTree s @?= Just t@@ -335,17 +241,17 @@ success "Foo Bar" $ TTApp (TTTerm "Foo") (TTTerm "Bar") success "Foo Bar Baz" $ TTApp (TTTerm "Foo") (TTTerm "Bar") `TTApp` TTTerm "Baz" -getRootR :: Text-getRootR = pack "this is the root"+getRootR :: HandlerFor site Text+getRootR = pure $ Text.pack "this is the root" -getBlogPostR :: Text -> String-getBlogPostR t = "some blog post: " ++ unpack t+getBlogPostR :: Text -> HandlerFor site String+getBlogPostR t = pure $ "some blog post: " ++ Text.unpack t -postBlogPostR :: Text -> Text-postBlogPostR t = pack $ "POST some blog post: " ++ unpack t+postBlogPostR :: Text -> HandlerFor site Text+postBlogPostR t = pure $ Text.pack $ "POST some blog post: " ++ Text.unpack t -handleWikiR :: [Text] -> String-handleWikiR ts = "the wiki: " ++ show ts+handleWikiR :: [Text] -> HandlerFor site String+handleWikiR ts = pure $ "the wiki: " ++ show ts -getChildR :: Text -> Text-getChildR = id+getChildR :: Text -> HandlerFor site Text+getChildR = pure
test/YesodCoreTest.hs view
@@ -1,24 +1,53 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-}+ module YesodCoreTest (specs) where import YesodCoreTest.CleanPath import YesodCoreTest.Exceptions import YesodCoreTest.Widget import YesodCoreTest.Media+import YesodCoreTest.Meta import YesodCoreTest.Links import YesodCoreTest.Header import YesodCoreTest.NoOverloadedStrings+import YesodCoreTest.SubSub+import YesodCoreTest.SplitSubsite.Runtime (splitSubsiteSpec)+import YesodCoreTest.ParameterizedSubData ()+import YesodCoreTest.ParamSubsite.Data ()+import YesodCoreTest.ParameterizedSubDispatch ()+import qualified YesodCoreTest.ParameterizedSubDispatchRuntime as ParameterizedSubDispatchRuntime+import qualified YesodCoreTest.ParamSubsite.InstanceRuntime as ParamSubsiteInstanceRuntime+import qualified YesodCoreTest.ParamSubsite.SplitRuntime as ParamSubsiteSplitRuntime+import qualified YesodCoreTest.ParamDefaultSplit.Runtime as ParamDefaultSplitRuntime+import qualified YesodCoreTest.ParamTopLevelRuntime as ParamTopLevelRuntime+import qualified YesodCoreTest.ParamFocusSplit.Runtime as ParamFocusSplit+import qualified YesodCoreTest.ParamNoExplicitArgs as ParamNoExplicitArgs+import qualified YesodCoreTest.ParamFallthroughRuntime as ParamFallthroughRuntime+import qualified YesodCoreTest.ParamNoFallthroughRuntime as ParamNoFallthroughRuntime+import qualified YesodCoreTest.ParamNestedNoFallthroughRuntime as ParamNestedNoFallthroughRuntime+import qualified YesodCoreTest.FallthroughMatrix.Runtime as FallthroughMatrixRuntime+import qualified YesodCoreTest.MultiPieceNestedRuntime as MultiPieceNestedRuntime+import qualified YesodCoreTest.ZeroPieceShadowRuntime as ZeroPieceShadowRuntime+import qualified YesodCoreTest.BangSeparatorRuntime as BangSeparatorRuntime+import qualified YesodCoreTest.SubsiteFallthrough.Runtime as SubsiteFallthrough+import qualified YesodCoreTest.SubsiteOptsFallthrough.Runtime as SubsiteOptsFallthrough import YesodCoreTest.InternalRequest import YesodCoreTest.ErrorHandling import YesodCoreTest.Cache+import YesodCoreTest.ParameterizedSite+import YesodCoreTest.Breadcrumb import qualified YesodCoreTest.WaiSubsite as WaiSubsite import qualified YesodCoreTest.Redirect as Redirect+import qualified YesodCoreTest.JsAttributes as JsAttributes import qualified YesodCoreTest.JsLoader as JsLoader import qualified YesodCoreTest.RequestBodySize as RequestBodySize import qualified YesodCoreTest.Json as Json+import qualified YesodCoreTest.Content as Content+import qualified YesodCoreTest.NestedDispatch.Runtime as NestedDispatch+import qualified YesodCoreTest.FallthroughDispatch.Runtime as FallthroughDispatch -- Skip on Windows, see https://github.com/yesodweb/yesod/issues/1523#issuecomment-398278450-#if !WINDOWS+#ifndef WINDOWS import qualified YesodCoreTest.RawResponse as RawResponse #endif @@ -40,15 +69,19 @@ mediaTest linksTest noOverloadedTest+ subSubTest+ splitSubsiteSpec internalRequestTest errorHandlingTest cacheTest+ parameterizedSiteTest WaiSubsite.specs Redirect.specs+ JsAttributes.specs JsLoader.specs RequestBodySize.specs Json.specs-#if !WINDOWS+#ifndef WINDOWS RawResponse.specs #endif Streaming.specs@@ -59,3 +92,26 @@ Ssl.sslOnlySpec Ssl.sameSiteSpec Csrf.csrfSpec+ breadcrumbTest+ metaTest+ Content.specs+ describe "NestedDispatch" $ do+ NestedDispatch.specs+ ParameterizedSubDispatchRuntime.specs+ ParamSubsiteInstanceRuntime.specs+ ParamSubsiteSplitRuntime.specs+ ParamDefaultSplitRuntime.specs+ ParamTopLevelRuntime.specs+ ParamFocusSplit.specs+ ParamNoExplicitArgs.specs+ ParamFallthroughRuntime.specs+ ParamNoFallthroughRuntime.specs+ ParamNestedNoFallthroughRuntime.specs+ FallthroughMatrixRuntime.specs+ MultiPieceNestedRuntime.specs+ ZeroPieceShadowRuntime.specs+ BangSeparatorRuntime.specs+ SubsiteFallthrough.specs+ SubsiteOptsFallthrough.specs+ describe "FallthroughDispatch" $ do+ FallthroughDispatch.spec
test/YesodCoreTest/Auth.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.Auth ( specs , Widget
+ test/YesodCoreTest/BangSeparatorRuntime.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | T2: a @!@-separated dynamic route (@\/!#Int@) is only ever exercised by the+-- render/parse specs (in the @test-routes@ suite's pure-routing @Hierarchy@+-- fixture). The @!@ flips /compile-time/ overlap checking only, so at runtime+-- @\/!#Int@ dispatches exactly like @\/#Int@. This pins that equivalence with a+-- real WAI round-trip.+module YesodCoreTest.BangSeparatorRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertGet)++data BangApp = BangApp++mkYesod "BangApp" [parseRoutes|+/!#Int BackwardsR GET+/plain/#Int PlainR GET+|]++instance Yesod BangApp where+ messageLoggerSource = mempty++getBackwardsR :: Int -> HandlerFor BangApp Text+getBackwardsR i = pure (toPathPiece i <> ":backwards")++getPlainR :: Int -> HandlerFor BangApp Text+getPlainR i = pure (toPathPiece i <> ":plain")++req :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+req = assertGet BangApp++specs :: Spec+specs = describe "!-separator dynamic route dispatch (/!#Int)" $ do+ it "dispatches BackwardsR on a numeric piece, like a plain /#Int" $+ req 200 ["5"] (Just "5:backwards")+ it "still 404s a non-numeric piece (the # parser is unchanged by !)" $+ req 404 ["notanint"] Nothing+ it "dispatches the plain /#Int sibling for contrast" $+ req 200 ["plain", "7"] (Just "7:plain")
+ test/YesodCoreTest/Breadcrumb.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module YesodCoreTest.Breadcrumb+ ( breadcrumbTest,+ )+where++import Data.Text (Text)+import Network.Wai+import Network.Wai.Test+import Test.Hspec+import Yesod.Core++data A = A++mkYesod+ "A"+ [parseRoutes|+/ RootR GET+/loop LoopR GET+|]++instance Yesod A++instance YesodBreadcrumbs A where+ breadcrumb r = case r of+ RootR -> pure ("Root", Nothing)+ LoopR -> pure ("Loop", Just LoopR) -- Purposefully a loop++getRootR :: Handler Text+getRootR = fst <$> breadcrumbs++getLoopR :: Handler Text+getLoopR = fst <$> breadcrumbs++breadcrumbTest :: Spec+breadcrumbTest =+ describe "Test.Breadcrumb" $ do+ it "can fetch the root which contains breadcrumbs" $+ runner $ do+ res <- request defaultRequest+ assertStatus 200 res+ it "gets a 500 for a route with a looping breadcrumb" $+ runner $ do+ res <- request defaultRequest {pathInfo = ["loop"]}+ assertStatus 500 res++runner :: Session () -> IO ()+runner f = toWaiApp A >>= runSession f
test/YesodCoreTest/Cache.hs view
@@ -1,8 +1,11 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE Rank2Types #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.Cache ( cacheTest , Widget@@ -22,10 +25,8 @@ data C = C newtype V1 = V1 Int- deriving Typeable newtype V2 = V2 Int- deriving Typeable mkYesod "C" [parseRoutes| / RootR GET
test/YesodCoreTest/CleanPath.hs view
@@ -1,7 +1,11 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleInstances, ViewPatterns #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.CleanPath ( cleanPathTest , Widget@@ -21,7 +25,6 @@ import qualified Data.Text.Encoding as TE import Control.Arrow ((***)) import Network.HTTP.Types (encodePath)-import Data.Monoid (mappend) import Data.Text.Encoding (encodeUtf8Builder) data Subsite = Subsite@@ -64,7 +67,7 @@ corrected = filter (not . TS.null) s joinPath Y ar pieces' qs' =- encodeUtf8Builder ar `Data.Monoid.mappend` encodePath pieces qs+ encodeUtf8Builder ar `mappend` encodePath pieces qs where pieces = if null pieces' then [""] else pieces' qs = map (TE.encodeUtf8 *** go) qs'
+ test/YesodCoreTest/Content.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}+module YesodCoreTest.Content (specs) where++import Test.Hspec++import Yesod.Core+import Yesod.Core.Types (TypedContent, typedContentToSnippet)+import Data.Text (Text, pack)+import Data.Text.Encoding (encodeUtf8)+import Data.ByteString (ByteString)++import qualified Data.Encoding as Enc+import qualified Data.Encoding.GB18030 as Enc+import qualified Data.Encoding.CP1251 as Enc+import qualified Data.Encoding.ShiftJIS as Enc+import qualified Data.Encoding.CP932 as Enc++fakeContent :: (String -> ByteString) -> String -> String -> TypedContent+fakeContent encode contentTypeString contentString =+ TypedContent+ (encode contentTypeString)+ (toContent $ encode contentString)++fakeUtf8Content :: String -> String -> TypedContent+fakeUtf8Content = fakeContent (encodeUtf8 . pack)++fakeGB18030Content :: String -> String -> TypedContent+fakeGB18030Content = fakeContent $ Enc.encodeStrictByteString Enc.GB18030++fakeCP1251Content :: String -> String -> TypedContent+fakeCP1251Content = fakeContent $ Enc.encodeStrictByteString Enc.CP1251++fakeShiftJISContent :: String -> String -> TypedContent+fakeShiftJISContent = fakeContent $ Enc.encodeStrictByteString Enc.ShiftJIS++fakeCP932Content :: String -> String -> TypedContent+fakeCP932Content = fakeContent $ Enc.encodeStrictByteString Enc.CP932++specs :: Spec+specs = describe "typedContentToSnippet" $ do+ it "does not serialize content that is apparently binary data" $ do+ let content = fakeUtf8Content "application/octet-stream" "fake"+ (typedContentToSnippet content 100) `shouldBe` Nothing++ it "serializes UTF-8 encoded JSON" $ do+ let content = fakeUtf8Content "application/json; charset=utf-8" "[1,2,3]"+ (typedContentToSnippet content 100) `shouldBe` (Just "[1,2,3]")++ it "serializes text with no specified encoding" $ do+ let content = fakeUtf8Content "text/css" "fake"+ (typedContentToSnippet content 100) `shouldBe` (Just "fake")++ it "serializes RSS with no specified encoding" $ do+ let content = fakeUtf8Content "application/rss" "fake"+ (typedContentToSnippet content 100) `shouldBe` (Just "fake")++ it "serializes Atom feeds with no specified encoding" $ do+ let content = fakeUtf8Content "application/atom" "fake"+ (typedContentToSnippet content 100) `shouldBe` (Just "fake")++ it "truncates long snippets" $ do+ let content = fakeUtf8Content "application/json; charset=utf-8" "[1,2,3]"+ (typedContentToSnippet content 2) `shouldBe` (Just "[1...+ 5")++ it "serializes GB18030 text" $ do+ let content = fakeGB18030Content "application/json; charset=GB18030" "[1,2,3]"+ (typedContentToSnippet content 100) `shouldBe` (Just "[1,2,3]")++ it "serializes CP1251 text" $ do+ let content = fakeCP1251Content "application/json; charset=windows-1251" "[1,2,3]"+ (typedContentToSnippet content 100) `shouldBe` (Just "[1,2,3]")++ it "serializes ShiftJIS text" $ do+ let content = fakeShiftJISContent "application/json; charset=Shift_JIS" "[1,2,3]"+ (typedContentToSnippet content 100) `shouldBe` (Just "[1,2,3]")++ it "serializes CP932 text" $ do+ let content = fakeCP932Content "application/json; charset=Windows-31J" "[1,2,3]"+ (typedContentToSnippet content 100) `shouldBe` (Just "[1,2,3]")
test/YesodCoreTest/Csrf.hs view
@@ -1,4 +1,8 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-} module YesodCoreTest.Csrf (csrfSpec, Widget, resourcesApp) where @@ -10,7 +14,6 @@ import Web.Cookie import qualified Data.Map as Map import Data.ByteString.Lazy (fromStrict)-import Data.Monoid ((<>)) data App = App @@ -79,7 +82,7 @@ res <- request (defaultRequest { requestMethod = "POST", requestHeaders = [(defaultCsrfHeaderName, csrfValue <> "foo")] }) assertStatus 403 res- + it "403s write requests with the wrong CSRF param" $ runner $ do getRes <- request defaultRequest assertStatus 200 getRes
test/YesodCoreTest/ErrorHandling.hs view
@@ -1,26 +1,38 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.ErrorHandling ( errorHandlingTest , Widget , resourcesApp ) where++import Data.Typeable(cast)+import qualified Control.Concurrent.Async as Async+import Control.Concurrent as Conc import Yesod.Core import Test.Hspec import Network.Wai import Network.Wai.Test import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Char8 as S8-import Control.Exception (SomeException, try)+import Control.Exception (SomeException, try, AsyncException(..)) import Network.HTTP.Types (Status, mkStatus) import Data.ByteString.Builder (Builder, toLazyByteString)-import Data.Monoid (mconcat) import Data.Text (Text, pack) import Control.Monad (forM_)+import qualified Network.Wai.Handler.Warp as Warp+import qualified YesodCoreTest.ErrorHandling.CustomApp as Custom import Control.Monad.Trans.State (StateT (..)) import Control.Monad.Trans.Reader (ReaderT (..)) import qualified UnliftIO.Exception as E+import System.Timeout(timeout) data App = App @@ -45,6 +57,10 @@ /auth-not-adequate AuthNotAdequateR GET /args-not-valid ArgsNotValidR POST /only-plain-text OnlyPlainTextR GET++/thread-killed ThreadKilledR GET+/connection-closed-by-peer ConnectionClosedPeerR GET+/sleep-sec SleepASecR GET |] overrideStatus :: Status@@ -106,11 +122,28 @@ getFileBadNameR = return $ TypedContent "ignored" $ ContentFile (error "filebadname") Nothing goodBuilderContent :: Builder-goodBuilderContent = Data.Monoid.mconcat $ replicate 100 $ "This is a test\n"+goodBuilderContent = mconcat $ replicate 100 $ "This is a test\n" getGoodBuilderR :: Handler TypedContent getGoodBuilderR = return $ TypedContent "text/plain" $ toContent goodBuilderContent +-- this handler kills it's own thread+getThreadKilledR :: Handler Html+getThreadKilledR = do+ x <- liftIO Conc.myThreadId+ liftIO $ Async.withAsync (Conc.killThread x) Async.wait+ pure "unreachablle"+getSleepASecR :: Handler Html+getSleepASecR = do+ liftIO $ Conc.threadDelay 1000000+ pure "slept a second"++getConnectionClosedPeerR :: Handler Html+getConnectionClosedPeerR = do+ x <- liftIO Conc.myThreadId+ liftIO $ Async.withAsync (E.throwTo x Warp.ConnectionClosedByPeer) Async.wait+ pure "unreachablle"+ getErrorR :: Int -> Handler () getErrorR 1 = setSession undefined "foo" getErrorR 2 = setSession "foo" undefined@@ -154,6 +187,10 @@ it "accept CSS, permission denied -> 403" caseCssPermissionDenied it "accept image, non-existent path -> 404" caseImageNotFound it "accept video, bad method -> 405" caseVideoBadMethod+ it "default config exception rethrows connection closed" caseDefaultConnectionCloseRethrows+ it "custom config rethrows an exception" caseCustomExceptionRethrows+ it "thread killed rethrow" caseThreadKilledRethrow+ it "can timeout a runner" canTimeoutARunner runner :: Session a -> IO a runner f = toWaiApp App >>= runSession f@@ -291,3 +328,50 @@ ("accept", "video/webm") : requestHeaders defaultRequest } assertStatus 405 res++fromExceptionUnwrap :: E.Exception e => SomeException -> Maybe e+fromExceptionUnwrap se+ | Just (E.AsyncExceptionWrapper e) <- E.fromException se = cast e+ | Just (E.SyncExceptionWrapper e) <- E.fromException se = cast e+ | otherwise = E.fromException se+++caseThreadKilledRethrow :: IO ()+caseThreadKilledRethrow =+ shouldThrow testcode $ \e -> case fromExceptionUnwrap e of+ (Just ThreadKilled) -> True+ _ -> False+ where+ testcode = runner $ do+ res <- request defaultRequest { pathInfo = ["thread-killed"] }+ assertStatus 500 res+ assertBodyContains "Internal Server Error" res++caseDefaultConnectionCloseRethrows :: IO ()+caseDefaultConnectionCloseRethrows =+ shouldThrow testcode $ \e -> case fromExceptionUnwrap e of+ Just Warp.ConnectionClosedByPeer -> True+ _ -> False++ where+ testcode = runner $ do+ _res <- request defaultRequest { pathInfo = ["connection-closed-by-peer"] }+ pure ()++caseCustomExceptionRethrows :: IO ()+caseCustomExceptionRethrows =+ shouldThrow testcode $ \case Custom.MkMyException -> True+ where+ testcode = customAppRunner $ do+ _res <- request defaultRequest { pathInfo = ["throw-custom-exception"] }+ pure ()+ customAppRunner f = toWaiApp Custom.CustomApp >>= runSession f+++canTimeoutARunner :: IO ()+canTimeoutARunner = do+ res <- timeout 1000 $ runner $ do+ res <- request defaultRequest { pathInfo = ["sleep-sec"] }+ assertStatus 200 res -- if 500, it's catching the timeout exception+ pure () -- it should've timeout by now, either being 500 or Nothing+ res `shouldBe` Nothing -- make sure that pure statement didn't happen.
+ test/YesodCoreTest/ErrorHandling/CustomApp.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | a custom app that throws an exception+module YesodCoreTest.ErrorHandling.CustomApp+ (CustomApp(..)+ , MyException(..)++ -- * unused+ , Widget+ , resourcesCustomApp+ ) where++import Yesod.Core+import qualified UnliftIO.Exception as E++data CustomApp = CustomApp++mkYesod "CustomApp" [parseRoutes|+/throw-custom-exception CustomHomeR GET+|]++getCustomHomeR :: Handler Html+getCustomHomeR =+ E.throwIO MkMyException++data MyException = MkMyException+ deriving (Show, E.Exception)++instance Yesod CustomApp where+ -- something we couldn't do before, rethrow custom exceptions+ catchHandlerExceptions _ action handler =+ action `E.catch` \exception -> do+ case E.fromException exception of+ Just MkMyException -> E.throwIO MkMyException+ Nothing -> handler exception
test/YesodCoreTest/Exceptions.hs view
@@ -1,6 +1,10 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.Exceptions ( exceptionsTest , Widget@@ -10,7 +14,7 @@ import Test.Hspec import Yesod.Core-import Yesod.Core.Types (HandlerContents (HCError))+import Yesod.Core.Types (TypedContent, HandlerContents (HCError)) import Control.Exception (throwIO) import Network.Wai import Network.Wai.Test
+ test/YesodCoreTest/FallthroughDispatch/Resources.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Shared route table + foundation type for "YesodCoreTest.FallthroughDispatch.Runtime".+-- Two parents share the @\/foo@ prefix (@FirstFooR@\/@SecondFooR@), plus+-- @\/foo\/#Int@ and @\/foo\/#String@ parents — the deliberate overlap is why it+-- uses @parseRoutesNoCheck@ (the compile-time overlap check would otherwise+-- reject the shared prefix that the fallthrough behaviour exists to handle).+module YesodCoreTest.FallthroughDispatch.Resources where++import Yesod.Core+import Yesod.Routes.TH.Types++data App = App++fallthroughDispatchResources :: [ResourceTree String]+fallthroughDispatchResources = [parseRoutesNoCheck|++/foo FirstFooR:+ / FooIndexR+ /neat FooNeatR++/foo SecondFooR:+ / NeverFiresR+ /other OtherR++/foo/#Int ParamFooR:+ / FooWithIntR++/foo/#String TextFooR:+ / FooWithTextR+|]
+ test/YesodCoreTest/FallthroughDispatch/Runtime.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++{-# OPTIONS_GHC -Wno-orphans #-}++-- | The single-module fallthrough contract demo for a top-level site, built+-- with @setNestedRouteFallthrough True@. Two same-prefix parents share @\/foo@:+-- @FirstFooR@ (with an index + @\/neat@) and @SecondFooR@ (@\/other@). With+-- fallthrough on, a request that misses inside @FirstFooR@ continues to the+-- sibling @SecondFooR@ instead of committing to @FirstFooR@'s 404 — the WAI+-- specs here pin exactly which parent serves each path. Route table in+-- ".FallthroughDispatch.Resources".+module YesodCoreTest.FallthroughDispatch.Runtime where++import YesodCoreTest.FallthroughDispatch.Resources+import YesodCoreTest.RuntimeHarness (assertGet)+import Yesod.Core+import Test.Hspec++mkYesodOpts (setNestedRouteFallthrough True defaultOpts) "App" fallthroughDispatchResources++instance Yesod App++handleFooIndexR :: HandlerFor app String+handleFooIndexR = pure "FooIndexR"++handleOtherR :: HandlerFor app String+handleOtherR = pure "OtherR"++handleFooNeatR :: HandlerFor app String+handleFooNeatR = pure "FooNeatR"++handleNeverFiresR :: HandlerFor app String+handleNeverFiresR = pure "NeverFiresR"++handleFooWithIntR :: Int -> HandlerFor app String+handleFooWithIntR i = pure $ "FooWithIntR " <> show i++handleFooWithTextR :: String -> HandlerFor app String+handleFooWithTextR str = pure $ "FooWithTextR " <> str++spec :: Spec+spec = do+ describe "FirstFooR" $ do+ it "FooIndexR" $ do+ assertGet App 200 ["foo"] (Just "FooIndexR")++ it "FooNeatR" $ do+ assertGet App 200 ["foo", "neat"] (Just "FooNeatR")++ describe "SecondFooR" $ do+ -- This is a little unsatisfying. Ideally we could write a test+ -- that expresses that, uh, `renderRoute (SecondFooR NeverFiresR)`+ -- would always direct to a different route. Difficult to do+ -- though.+ it "NeverFiresR" $ do+ assertGet App 200 ["foo"] (Just "FooIndexR")++ it "OtherR" $ do+ assertGet App 200 ["foo", "other"] (Just "OtherR")++ describe "ParamFooR" $ do+ it "FooWithIntR" $ do+ assertGet App 200 ["foo", "3"] (Just "FooWithIntR 3")++ describe "TextFooR" $ do+ it "FooWithTextR" $ do+ assertGet App 200 ["foo", "asdf"] (Just "FooWithTextR asdf")
+ test/YesodCoreTest/FallthroughMatrix/FirstFoo.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | The split-out nested-dispatch instance for @FirstFooR@, generated with the+-- DEFAULT route options — i.e. @fallthrough = False@. The @FooIndexR@\/@FooNeatR@+-- handlers live ONLY here, so the top module ("YesodCoreTest.FallthroughMatrix.Runtime") can+-- compile only by delegating to the @YesodDispatchNested FirstFooR@ instance+-- generated by this splice (it cannot inline handlers it can't see).+--+-- This module's flag is @False@, but a nested instance must still report a+-- path miss as 'Nothing' rather than baking a 404 into its own terminal+-- fallback — the fallthrough decision belongs to whichever module delegates+-- to it. An instance that 404s on its own would silently defeat the top+-- module's @fallthrough = True@ intent.+module YesodCoreTest.FallthroughMatrix.FirstFoo where++import Yesod.Core+import YesodCoreTest.FallthroughMatrix.Resources++mkYesodOpts (setFocusOnNestedRoute "FirstFooR" defaultOpts) "FMApp" fmResources++getFooIndexR :: HandlerFor FMApp String+getFooIndexR = pure "FooIndexR"++getFooNeatR :: HandlerFor FMApp String+getFooNeatR = pure "FooNeatR"
+ test/YesodCoreTest/FallthroughMatrix/Resources.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Shared route table + site type for the cross-module fallthrough matrix.+--+-- Two parents share the @\/foo@ prefix: @FirstFooR@ (whose nested instance is+-- generated in a /separate/ module, "YesodCoreTest.FallthroughMatrix.FirstFoo",+-- with the default @fallthrough = False@) and @SecondFooR@ (inlined in the+-- top module "YesodCoreTest.FallthroughMatrix.Runtime", which sets+-- @fallthrough = True@). The flag mismatch across the module split is the whole+-- point: it lets us pin who owns the terminal 404 on @\/foo\/other@.+--+-- @FirstFooR@ owns @\/foo\/neat@ as @GET@-only; @SecondFooR@ owns the same+-- @\/foo\/neat@ path as @POST@-only. This shared @neat@ leaf is what pins the+-- wrong-method contract under fallthrough: a @POST \/foo\/neat@ matches+-- @FirstFooR@'s @GET@ leaf /by path/ and so must commit to 405 — fallthrough is+-- only triggered by a path miss ('Nothing'), and a matched-path/wrong-method+-- leaf returns @Just \<405\>@, never 'Nothing'. If it instead fell through to+-- @SecondFooR@'s @POST@ leaf it would 200, which would be a contract change.+module YesodCoreTest.FallthroughMatrix.Resources where++import Yesod.Core+import Yesod.Routes.TH.Types++data FMApp = FMApp++fmResources :: [ResourceTree String]+fmResources = [parseRoutesNoCheck|+/foo FirstFooR:+ / FooIndexR GET+ /neat FooNeatR GET++/foo SecondFooR:+ / NeverFiresR GET+ /other OtherR GET+ /neat NeatPostR POST+|]
+ test/YesodCoreTest/FallthroughMatrix/Runtime.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Cross-module fallthrough flag mismatch: the delegating module and the+-- split-out nested instance are compiled with opposite fallthrough flags.+--+-- This top module sets @fallthrough = True@ and delegates @FirstFooR@ to the+-- nested instance generated (with the default @fallthrough = False@) in+-- "YesodCoreTest.FallthroughMatrix.FirstFoo". @SecondFooR@ is inlined here.+--+-- The single-module analogue "YesodCoreTest.ParamFallthroughRuntime" proves+-- that with fallthrough enabled, @\/foo\/other@ falls through @FirstFooR@ (which+-- has no @other@ child) to @SecondFooR@ → @OtherR@ (200). This module is the+-- same shape but with @FirstFooR@'s instance split into a module compiled with+-- the opposite flag — the configuration that only works because a nested+-- instance reports a path miss as 'Nothing' instead of baking in its own 404,+-- leaving the fallthrough decision to the module that delegates to it.+module YesodCoreTest.FallthroughMatrix.Runtime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.FallthroughMatrix.Resources+import YesodCoreTest.FallthroughMatrix.FirstFoo (FirstFooR(..))+import YesodCoreTest.RuntimeHarness (assertGet, assertRequestFor)++mkYesodOpts (setNestedRouteFallthrough True defaultOpts) "FMApp" fmResources++instance Yesod FMApp where+ messageLoggerSource = mempty++getNeverFiresR :: HandlerFor FMApp String+getNeverFiresR = pure "NeverFiresR"++getOtherR :: HandlerFor FMApp String+getOtherR = pure "OtherR"++postNeatPostR :: HandlerFor FMApp String+postNeatPostR = pure "NeatPostR"++testRequestIO :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+testRequestIO = assertGet FMApp++specs :: Spec+specs = describe "cross-module fallthrough flag mismatch (top True, split child False)" $ do+ it "matches the split child's index (FooIndexR at /foo)" $+ testRequestIO 200 ["foo"] (Just "FooIndexR")+ it "matches a child of the split parent (FooNeatR at /foo/neat)" $+ testRequestIO 200 ["foo", "neat"] (Just "FooNeatR")+ it "falls through the split parent to the inline sibling (OtherR at /foo/other)" $+ -- The headline case. The top module wants fallthrough; FirstFooR has+ -- no "other" child, so dispatch falls through to SecondFooR's OtherR+ -- (200). If FirstFooR's split instance (compiled with flag False)+ -- instead baked a 404 into its own terminal fallback, it would defeat+ -- the parent's fallthrough.+ testRequestIO 200 ["foo", "other"] (Just "OtherR")++ -- Terminal semantics of fallthrough. Fallthrough is triggered+ -- ONLY by a path-level miss (a 'Nothing' from the matched parent): a+ -- matched leaf — even one rejecting the method — returns @Just <405>@, so+ -- dispatch commits to that leaf and never falls through to a same-prefix+ -- sibling. These rows pin both halves of that contract.+ describe "wrong method on a matched leaf commits (does not fall through)" $ do+ it "POST /foo/neat commits to 405 at FirstFooR's GET leaf" $+ -- FirstFooR owns /foo/neat as GET-only and SecondFooR owns the same+ -- /foo/neat path as POST-only. A POST matches FirstFooR's leaf by+ -- PATH, so the split (fallthrough=False) FirstFooR instance returns+ -- Just <405> — it does NOT return Nothing — and the host's+ -- fallthrough arm therefore commits to 405 rather than falling+ -- through to SecondFooR's POST leaf (which would be 200). If this+ -- ever flips to 200, fallthrough has started swallowing a matched+ -- leaf's method rejection, which is a contract change.+ assertRequestFor FMApp "POST" 405 ["foo", "neat"] Nothing+ it "GET /foo/neat still serves FirstFooR's GET leaf (200)" $+ -- The mirror of the above: the same shared path under the right+ -- method resolves to the first matching leaf, never SecondFooR.+ testRequestIO 200 ["foo", "neat"] (Just "FooNeatR")++ describe "total miss under fallthrough lands on the terminal 404" $ do+ it "GET /foo/zzz exhausts both same-prefix parents -> 404" $+ -- /foo/zzz matches the /foo prefix of both FirstFooR and SecondFooR+ -- but no leaf of either. Each parent reports a path miss ('Nothing'),+ -- so dispatch falls through every sibling and lands on the terminal+ -- 404 clause ("except in the final case" from the flag's haddock).+ testRequestIO 404 ["foo", "zzz"] Nothing+ it "GET /zzz matches no parent prefix at all -> 404" $+ -- A top-level total miss: no parent's prefix matches, so the only+ -- clause that fires is the terminal 404.+ testRequestIO 404 ["zzz"] Nothing
test/YesodCoreTest/Header.hs view
@@ -1,5 +1,9 @@-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes,- TypeFamilies, MultiParamTypeClasses, ViewPatterns #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-} module YesodCoreTest.Header ( headerTest
test/YesodCoreTest/InternalRequest.hs view
@@ -1,20 +1,20 @@ {-# LANGUAGE OverloadedStrings #-}+ module YesodCoreTest.InternalRequest (internalRequestTest) where import Data.List (nub) import Network.Wai as W import Yesod.Core.Internal (randomString, parseWaiRequest) import Test.Hspec-import Data.Monoid (mempty) import Data.Map (singleton) import Yesod.Core import Data.Word (Word64) import System.IO.Unsafe (unsafePerformIO) import Control.Monad (replicateM)-import System.Random+import System.Random (randomIO) gen :: IO Int-gen = getStdRandom next+gen = randomIO randomStringSpecs :: Spec randomStringSpecs = describe "Yesod.Internal.Request.randomString" $ do@@ -57,7 +57,7 @@ noDisabledToken :: Bool noDisabledToken = reqToken r == Nothing where- r = parseWaiRequest' defaultRequest Data.Monoid.mempty False 1000+ r = parseWaiRequest' defaultRequest mempty False 1000 ignoreDisabledToken :: Bool ignoreDisabledToken = reqToken r == Nothing where
+ test/YesodCoreTest/JsAttributes.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module YesodCoreTest.JsAttributes+ ( specs+ -- To avoid unused warning+ , Widget+ , resourcesApp+ ) where++import Test.Hspec++import Yesod.Core+import Network.Wai.Test++data App = App+mkYesod "App" [parseRoutes|+/ HeadR GET+|]+instance Yesod App where+ jsAttributes _ = [("attr0", "a")]++getHeadR :: Handler Html+getHeadR = defaultLayout $ do+ addScriptRemote "load.js"+ toWidget [julius|/*body*/|]+ toWidgetHead [julius|/*head*/|]++specs :: Spec+specs = describe "Test.JsAttributes" $ do+ it "script in body gets attributes" $ runner App $ do+ res <- request defaultRequest+ assertBody "<!DOCTYPE html>\n<html><head><title></title><script>/*head*/</script></head><body><script src=\"load.js\"></script><script attr0=\"a\">/*body*/</script></body></html>" res++runner :: YesodDispatch master => master -> Session () -> IO ()+runner app f = toWaiApp app >>= runSession f
test/YesodCoreTest/JsLoader.hs view
@@ -1,6 +1,10 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.JsLoader ( specs , Widget
test/YesodCoreTest/JsLoaderSites/Bottom.hs view
@@ -1,6 +1,10 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.JsLoaderSites.Bottom ( B(..) , Widget
test/YesodCoreTest/Json.hs view
@@ -1,4 +1,10 @@-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses, ViewPatterns #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.Json ( specs , Widget
test/YesodCoreTest/Links.hs view
@@ -1,6 +1,11 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleInstances, ViewPatterns #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.Links ( linksTest , Widget
test/YesodCoreTest/LiteApp.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module YesodCoreTest.LiteApp (specs) where import Yesod.Core
test/YesodCoreTest/Media.hs view
@@ -1,7 +1,12 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleInstances, ViewPatterns #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-}+ module YesodCoreTest.Media (mediaTest, Widget) where import Test.Hspec
test/YesodCoreTest/MediaData.hs view
@@ -1,6 +1,10 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.MediaData where import Yesod.Core
+ test/YesodCoreTest/Meta.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module YesodCoreTest.Meta+ ( metaTest+ ) where++import Test.Hspec++import Yesod.Core+import Network.Wai+import Network.Wai.Test++data App = App++mkYesod "App" [parseRoutes|+/title TitleR GET+/desc DescriptionR GET+|]++instance Yesod App where++getTitleR :: Handler Html+getTitleR = defaultLayout $ do+ setTitle "First title"+ setTitle "Second title"++getDescriptionR :: Handler Html+getDescriptionR = defaultLayout $ do+ setDescriptionIdemp "First description"+ setDescriptionIdemp "Second description"++metaTest :: Spec+metaTest = describe "Setting page metadata" $ do+ describe "Yesod.Core.Widget.setTitle" $ do+ it "is idempotent" $ runner $ do+ res <- request defaultRequest+ { pathInfo = ["title"]+ }+ assertBody "<!DOCTYPE html>\n<html><head><title>Second title</title></head><body></body></html>" res+ describe "Yesod.Core.Widget.setDescriptionIdemp" $ do+ it "is idempotent" $ runner $ do+ res <- request defaultRequest+ { pathInfo = ["desc"]+ }+ assertBody "<!DOCTYPE html>\n<html><head><title></title><meta name=\"description\" content=\"Second description\"></head><body></body></html>" res++runner :: Session () -> IO ()+runner f = toWaiAppPlain App >>= runSession f
+ test/YesodCoreTest/MultiPieceNestedRuntime.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | T3: a nested 'ResourceParent' whose OWN path pieces number more than one+-- and include a dynamic capture (@\/shop\/#Int\/admin@). Most nested-route tests+-- use single-piece parents; this stresses the multi-piece machinery —+-- 'handlePiecesNames' binding several pieces, 'mkPathPat' threading them, and+-- the @parentArgsExp@ tuple build that forwards the captured @#Int@ down to the+-- delegated child dispatch — and asserts both the round-trip and the misses.+module YesodCoreTest.MultiPieceNestedRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertGet)++data MPApp = MPApp++mkYesod "MPApp" [parseRoutes|+/shop/#Int/admin ShopAdminR:+ / ShopAdminIndexR GET+ /users/#Text UserR GET+|]++instance Yesod MPApp where+ messageLoggerSource = mempty++getShopAdminIndexR :: Int -> HandlerFor MPApp String+getShopAdminIndexR shopId = pure ("admin:" <> show shopId)++getUserR :: Int -> Text -> HandlerFor MPApp String+getUserR shopId name = pure ("user:" <> show shopId <> ":" <> show name)++testRequestIO :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+testRequestIO = assertGet MPApp++specs :: Spec+specs = describe "multi-piece nested parent (/shop/#Int/admin)" $ do+ it "dispatches the parent index, forwarding the captured #Int" $+ testRequestIO 200 ["shop", "5", "admin"] (Just "admin:5")+ it "dispatches a child, forwarding both the parent #Int and child #Text" $+ testRequestIO 200 ["shop", "5", "admin", "users", "bob"] (Just "user:5:\"bob\"")+ it "404s on an unknown suffix under the multi-piece parent" $+ testRequestIO 404 ["shop", "5", "admin", "nope"] Nothing+ it "404s when the parent's dynamic piece is malformed (#Int given a non-integer)" $+ testRequestIO 404 ["shop", "notanint", "admin"] Nothing
+ test/YesodCoreTest/NestedDispatch/InnerR.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Bug-2 regression fixture: @InnerR@ is split out into its own focused+-- module (so a @RouteAttrsNested InnerR@ instance is in scope), while its+-- parent @OuterR@ is /not/ split — the main @App@ module inlines @OuterR@.+--+-- When the full @RouteAttrs App@ instance descends through the inlined+-- @OuterR@ and reaches the delegated @InnerR@, the generated @routeAttrs@+-- clause must apply the accumulated ancestor pattern (@front@), producing+-- @routeAttrs (OuterR (InnerR _ x)) = routeAttrsNested x@. Dropping @front@+-- (the bug) would emit @routeAttrs (InnerR x) = …@, which fails to compile+-- because @Route App@ has no top-level @InnerR@ constructor.+module YesodCoreTest.NestedDispatch.InnerR where++import Data.Text (Text)+import YesodCoreTest.NestedDispatch.Resources+import Yesod.Core++mkYesodOpts (setFocusOnNestedRoute "InnerR" defaultOpts) "App" nestedDispatchResources++getInnerIndexR :: HandlerFor App Text+getInnerIndexR = pure "getInnerIndexR"
+ test/YesodCoreTest/NestedDispatch/NestR.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Split-out @NestR@ fragment of the "YesodCoreTest.NestedDispatch.Runtime" demo. The+-- @setFocusOnNestedRoute "NestR"@ splice generates the @NestR@ datatype and its+-- @YesodDispatchNested@ instance here; the @App@ module delegates to it. The+-- single @NestIndexR@ leaf handles both GET and POST, covering a zero-piece+-- nested index reached through delegation.+module YesodCoreTest.NestedDispatch.NestR where++import Yesod.Core+import Data.Text (Text)+import YesodCoreTest.NestedDispatch.Resources++mkYesodOpts (setFocusOnNestedRoute "NestR" defaultOpts) "App" nestedDispatchResources++getNestIndexR :: HandlerFor App Text+getNestIndexR = pure "getNestIndexR"++postNestIndexR :: HandlerFor App String+postNestIndexR = pure "hello"
+ test/YesodCoreTest/NestedDispatch/Parent0R.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+++-- | Split-out @Parent0R@ fragment of the "YesodCoreTest.NestedDispatch.Runtime" demo.+-- Besides delegation, this module pins two non-obvious cases: a multipiece+-- (@*Texts@) leaf under a split-out parent (see 'getFilesR' below), and a+-- /grandchild/ parent @Child0R@ nested two levels deep. This focused splice+-- generates both @Parent0R@ and its grandchild @Child0R@ datatype\/instance;+-- the grandchild's leaf /handlers/ live in+-- "YesodCoreTest.NestedDispatch.Parent0R.Child0R".+module YesodCoreTest.NestedDispatch.Parent0R where++import Data.Text (Text)+import qualified Data.Text as Text+import Yesod.Core.Handler+import YesodCoreTest.NestedDispatch.Resources+import Yesod.Core+import YesodCoreTest.NestedDispatch.Parent0R.Child0R++mkYesodOpts (setFocusOnNestedRoute "Parent0R" defaultOpts) "App" nestedDispatchResources++getParent0IndexR :: Int -> HandlerFor App Text+getParent0IndexR = pure . Text.pack . show++-- | A multi-piece (@*Texts@) leaf nested under a split-out parent. This is the+-- regression fixture for the nested-dispatch generator dropping the trailing+-- multipiece (it hardcoded 'EndExact' and built the constructor one arg short),+-- which fails to compile this focused dispatch unless the multi is bound,+-- appended to the route constructor, and forwarded to the handler.+getFilesR :: Int -> [Text] -> HandlerFor App Text+getFilesR i ts = return (Text.pack (show i) <> ":" <> Text.pack (show ts))
+ test/YesodCoreTest/NestedDispatch/Parent0R/Child0R.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Leaf handlers for the grandchild parent @Child0R@ of the+-- "YesodCoreTest.NestedDispatch.Runtime" demo. Non-obvious: this module defines /only/+-- the handlers — the @Child0R@ datatype and its dispatch instance are emitted+-- by the focused @Parent0R@ splice (one level up), since focusing on a parent+-- also generates its descendants' nested instances.+module YesodCoreTest.NestedDispatch.Parent0R.Child0R where++import Data.Text (Text)+import qualified Data.Text as Text+import Yesod.Core.Handler+import YesodCoreTest.NestedDispatch.Resources+import Yesod.Core++postParentChildIndexR :: Int -> Text -> HandlerFor App Text+postParentChildIndexR i t = pure $ Text.pack $ show (i, t)++getParentChildIndexR :: Int -> Text -> HandlerFor App Text+getParentChildIndexR = postParentChildIndexR++postParentChildR :: Int -> Text -> String -> HandlerFor App Text+postParentChildR i t c = pure $ Text.pack $ show (i, t, c)++getParentChildR :: Int -> Text -> String -> HandlerFor App Text+getParentChildR = postParentChildR
+ test/YesodCoreTest/NestedDispatch/ParentR.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Split-out @ParentR@ fragment of the "YesodCoreTest.NestedDispatch.Runtime" demo.+-- The @setFocusOnNestedRoute "ParentR"@ splice emits the @ParentR@ datatype and+-- its @YesodDispatchNested@ instance here, alongside the child handlers; the+-- main @App@ module reaches these only by delegation. Exercises a dynamic+-- parent piece (@\/parent\/#Int@) feeding its captured arg to the children.+module YesodCoreTest.NestedDispatch.ParentR where++import Data.Text (Text)+import qualified Data.Text as Text+import Yesod.Core.Handler+import YesodCoreTest.NestedDispatch.Resources+import Yesod.Core++mkYesodOpts (setFocusOnNestedRoute "ParentR" defaultOpts) "App" nestedDispatchResources++handleChild1R :: Int -> Text -> HandlerFor App Text+handleChild1R i t = return (Text.pack (show i) <> t)++getChild2R :: Int -> Int -> HandlerFor App Text+getChild2R i0 i1 = return ("GET" <> Text.pack (show (i0, i1)))++postChild2R :: Int -> Int -> HandlerFor App Text+postChild2R i0 i1 = return ("POST" <> Text.pack (show (i0, i1)))
+ test/YesodCoreTest/NestedDispatch/Resources.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Shared route table + foundation type for the "YesodCoreTest.NestedDispatch.Runtime"+-- demo and its split-out parent modules. Lives in its own module so every+-- @setFocusOnNestedRoute@ splice (NestR, ParentR, Parent0R, ...) can refer to+-- the same @nestedDispatchResources@ tree without tripping the TH stage+-- restriction. The tree deliberately mixes flat routes, single- and+-- multi-piece dynamic parents, a multipiece (@*Texts@) leaf, and same-prefix+-- siblings to exercise the breadth of the nested-dispatch generator.+module YesodCoreTest.NestedDispatch.Resources where++import Yesod.Core+import Yesod.Routes.TH.Types++data App = App++nestedDispatchResources :: [ResourceTree String]+nestedDispatchResources = [parseRoutes|+/ HomeR GET !home+/json JsonR GET++/redirectparent RedirectParentR GET+/redirecttext RedirectTextR GET+/redirectstring RedirectStringR GET+/redirectparams RedirectParamsR GET+/nest NestR:+ / NestIndexR GET POST++/parent/#Int ParentR:+ /#Text/child1 Child1R !child+ /#Int/child2 Child2R !child GET POST++/parent0/#Int Parent0R:+ / Parent0IndexR GET++ /files/*Texts FilesR GET++ /child0/#Text Child0R:+ / ParentChildIndexR GET POST+ /#String ParentChildR GET POST++/nest OtherNestR:+ /foo NestFooR GET++/outer OuterR:+ /inner InnerR:+ / InnerIndexR GET !innerleaf++/blah BlahR:+ / BlahIndexR GET++/robots.txt RobotsR:+ / RobotsIndexR+|]
+ test/YesodCoreTest/NestedDispatch/Runtime.hs view
@@ -0,0 +1,599 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++{-# OPTIONS_GHC -Wno-orphans #-}++-- | The headline nested-route-dispatch demo. @mkYesod "App"@ here generates a+-- @YesodDispatch App@ whose flat dispatch /delegates/ to a per-parent+-- @YesodDispatchNested@ instance for every @ResourceParent@ in the tree (NestR,+-- ParentR, Parent0R, OuterR, ...). Each such instance is generated in-splice by+-- this module, /unless/ a sibling module already provides it: NestR\/ParentR\/+-- Parent0R\/Child0R are split out (each via @setFocusOnNestedRoute@) so their+-- handlers and instances live elsewhere and are only reached through delegation.+-- The .Resources module holds the shared route table; the WAI specs here drive+-- real requests through the delegated dispatch.+module YesodCoreTest.NestedDispatch.Runtime+ ( specs+ , Widget+ , resourcesApp+ ) where++import Yesod.Core+import Yesod.Core.Class.Dispatch.ToParentRoute (toParentRoute)+import Test.Hspec+import Network.Wai+import Network.Wai.Test+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Char8 as S8+import Data.String (IsString)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Maybe (fromJust)+import Data.Proxy (Proxy (..))+import Data.Monoid (Endo (..))+import qualified Control.Monad.Trans.Writer as Writer+import qualified Data.Set as Set+import YesodCoreTest.NestedDispatch.Resources+import YesodCoreTest.NestedDispatch.NestR (NestR(..))+import YesodCoreTest.NestedDispatch.InnerR (InnerR(..))+import YesodCoreTest.NestedDispatch.ParentR (ParentR(..))+import YesodCoreTest.NestedDispatch.Parent0R (Parent0R(..), Child0R(..))+import YesodCoreTest.NestedDispatch.Parent0R.Child0R+import YesodCoreTest.RuntimeHarness (assertRequestRaw)+import qualified Network.HTTP.Types as H++mkYesod "App" nestedDispatchResources++getBlahIndexR :: HandlerFor App Text+getBlahIndexR = pure "getBlahIndexR"++instance Yesod App where+ messageLoggerSource = mempty++specialHtml :: IsString a => a+specialHtml = "text/html; charset=special"++tshow :: Show a => a -> Text+tshow = Text.pack . show++handleRobotsIndexR :: HandlerFor site Text+handleRobotsIndexR = pure "robots.txt"++getHomeR :: Handler TypedContent+getHomeR = selectRep $ do+ rep typeHtml "HTML"+ rep specialHtml "HTMLSPECIAL"+ rep typeXml "XML"+ rep typeJson "JSON"++getNestFooR :: Handler ()+getNestFooR = pure ()++-- | Redirects through the 'RedirectUrl' instance for 'WithParentArgs', which+-- routes via @toTextUrl (toParentRoute args url)@. Pins that the Location+-- header for a nested fragment is rendered identically to the full route.+getRedirectParentR :: Handler ()+getRedirectParentR = redirect (WithParentArgs (1 :: Int) (Child1R "hello"))++-- | Exercises @RedirectUrl master Text@.+getRedirectTextR :: Handler ()+getRedirectTextR = redirect ("/some/where" :: Text)++-- | Exercises @RedirectUrl master String@.+getRedirectStringR :: Handler ()+getRedirectStringR = redirect ("/some/string" :: String)++-- | Exercises @RedirectUrl master (Route master, [(Text, Text)])@.+getRedirectParamsR :: Handler ()+getRedirectParamsR = redirect (JsonR, [("q", "1")] :: [(Text, Text)])++rep :: Monad m => ContentType -> Text -> Writer.Writer (Data.Monoid.Endo [ProvidedRep m]) ()+rep ct t = provideRepType ct $ return (t :: Text)++getJsonR :: Handler TypedContent+getJsonR = selectRep $ do+ rep typeHtml "HTML"+ provideRep $ return $ object ["message" .= ("Invalid Login" :: Text)]++testRequest :: Int -- ^ http status code+ -> Request+ -> ByteString -- ^ expected body+ -> Spec+testRequest status req expected = it (S8.unpack $ fromJust $ lookup "Accept" $ requestHeaders req) $ do+ testRequestIO status req (Just expected)++testRequestIO :: HasCallStack => Int -- ^ http status code+ -> Request+ -> Maybe ByteString -- ^ expected body+ -> IO ()+testRequestIO status req mexpected =+ assertRequestRaw (toWaiApp App) req status mexpected++test :: String -- ^ accept header+ -> ByteString -- ^ expected body+ -> Spec+test accept expected =+ testRequest 200 (acceptRequest accept) expected++acceptRequest :: String -> Request+acceptRequest accept = defaultRequest+ { requestHeaders = [("Accept", S8.pack accept)]+ }++-- | Build a WAI app rooted at a nested route fragment (via the public+-- 'toWaiAppPlainNested' entry point), issue one GET at @path@, and return the+-- status code.+nestedStatusFor+ :: (ParentSite route ~ App, YesodDispatchNested route, ToParentRoute route)+ => Proxy route -> ParentArgs route -> [Text] -> IO Int+nestedStatusFor proxy parentArgs path = do+ app <- toWaiAppPlainNested proxy parentArgs App+ sres <- flip runSession app $ request defaultRequest { pathInfo = path }+ pure $ H.statusCode (simpleStatus sres)+++specs :: Spec+specs = do+ describe "Dispatch" $ do+ describe "properly does nested dispatch" $ do+ describe "NestR" $ do+ it "GET" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["nest"]+ , requestMethod = "GET"+ }+ (Just "getNestIndexR")+ it "POST" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["nest"]+ , requestMethod = "POST"+ }+ (Just "hello")+ it "invalid route" $ do+ testRequestIO+ 404+ defaultRequest+ { pathInfo = ["nest", "oops"]+ , requestMethod = "GET"+ }+ Nothing+ it "invalid method" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo = ["nest"]+ , requestMethod = "PUT"+ }+ Nothing++ describe "ParentR" $ do+ describe "Child1R" $ do+ it "works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent", tshow @Int 1, "hello", "child1"]+ }+ (Just "1hello")++ describe "Child2R" $ do+ it "GET" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent", tshow @Int 1, tshow @Int 3, "child2"]+ , requestMethod = "GET"+ }+ (Just "GET(1,3)")+ it "POST" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent", tshow @Int 1, tshow @Int 3, "child2"]+ , requestMethod = "POST"+ }+ (Just "POST(1,3)")+ it "PUT" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo =+ ["parent", tshow @Int 1, tshow @Int 3, "child2"]+ , requestMethod = "PUT"+ }+ Nothing++ describe "Parent0R" $ do+ describe "FilesR (multi-piece *Texts leaf under a split parent)" $ do+ it "dispatches a non-empty tail, forwarding parent #Int and the multipiece" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["parent0", tshow @Int 7, "files", "a", "b"]+ , requestMethod = "GET"+ }+ (Just "7:[\"a\",\"b\"]")+ it "dispatches an empty tail as the empty multipiece" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["parent0", tshow @Int 7, "files"]+ , requestMethod = "GET"+ }+ (Just "7:[]")+ it "405s on a non-GET method" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo = ["parent0", tshow @Int 7, "files", "a", "b"]+ , requestMethod = "POST"+ }+ Nothing++ describe "Parent0IndexR" $ do+ it "GET works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1]+ , requestMethod = "GET"+ }+ (Just "1")+ it "POST errors" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1]+ , requestMethod = "POST"+ }+ Nothing++ describe "Child0R" $ do+ describe "ParentChildIndexR" $ do+ it "GET works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1, "child0", "asdf"]+ , requestMethod = "GET"+ }+ (Just "(1,\"asdf\")")+ it "POST works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 2, "child0", "asdf"]+ , requestMethod = "POST"+ }+ (Just "(2,\"asdf\")")+ it "PUT errors" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 2, "child0", "asdf"]+ , requestMethod = "PUT"+ }+ Nothing++ describe "ParentChildR" $ do+ it "GET works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1, "child0", "asdf", "foobar"]+ , requestMethod = "GET"+ }+ (Just "(1,\"asdf\",\"foobar\")")+ it "POST works" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1, "child0", "asdf", "foobar"]+ , requestMethod = "POST"+ }+ (Just "(1,\"asdf\",\"foobar\")")+ it "PUT fails" $ do+ testRequestIO+ 405+ defaultRequest+ { pathInfo =+ ["parent0", tshow @Int 1, "child0", "asdf", "foobar"]+ , requestMethod = "PUT"+ }+ Nothing++ describe "trailing-slash redirect (cleanPath) for a nested route" $ do+ it "301s a trailing slash on a GET to a split nested route" $ do+ testRequestIO+ 301+ defaultRequest+ { pathInfo = ["parent", tshow @Int 1, tshow @Int 3, "child2", ""]+ , requestMethod = "GET"+ }+ Nothing++ describe "nested-dispatch entry points (miss / deep 404)" $ do+ -- These exercise toWaiAppPlainNested/toWaiAppYreNested directly, not just+ -- the happy path. In particular they cover the `Nothing -> notFound`+ -- branch in toWaiAppYreNested (a fragment instance returning Nothing on a+ -- non-match) and confirm a `drop parentDepth` against a too-short pathInfo+ -- yields a clean 404 rather than a mis-dispatch.+ describe "rooted at NestR (no dynamic prefix, depth 1)" $ do+ it "dispatches the matching index path" $+ nestedStatusFor (Proxy :: Proxy NestR) () ["nest"]+ `shouldReturn` 200+ it "404s a non-matching tail through the Nothing -> notFound branch" $+ nestedStatusFor (Proxy :: Proxy NestR) () ["nest", "oops"]+ `shouldReturn` 404++ describe "rooted at a deep dynamic parent (ParentR/#Int, depth 2)" $ do+ it "dispatches a full matching path" $+ nestedStatusFor (Proxy :: Proxy ParentR) (1 :: Int)+ ["parent", "1", "hello", "child1"]+ `shouldReturn` 200+ it "cleanly 404s a pathInfo shorter than the parent depth" $+ nestedStatusFor (Proxy :: Proxy ParentR) (1 :: Int) ["parent"]+ `shouldReturn` 404+ it "cleanly 404s an empty pathInfo" $+ nestedStatusFor (Proxy :: Proxy ParentR) (1 :: Int) []+ `shouldReturn` 404+ it "404s a non-matching deep tail" $+ nestedStatusFor (Proxy :: Proxy ParentR) (1 :: Int)+ ["parent", "1", "nope"]+ `shouldReturn` 404++ describe "UrlToDispatch NestIndexR" $ do+ it "works" $ do+ yre <- mkYesodRunnerEnv App+ let app = urlToDispatch NestIndexR yre+ req = defaultRequest+ { pathInfo = ["nest"]+ , requestMethod = "GET"+ }+ sres <- flip runSession app $ do+ request req+ H.statusCode (simpleStatus sres) `shouldBe` 200+ simpleBody sres `shouldBe` "getNestIndexR"++ describe "UrlToDispatch (WithParentArgs route)" $ do+ -- Exercises the only UrlToDispatch instance with real args-threading+ -- logic (Yesod.Core.Class.Dispatch): a fragment plus its dynamic parent+ -- args is turned into a WAI app that dispatches into the right handler.+ it "dispatches a dynamic-parent fragment to the right handler (200)" $ do+ yre <- mkYesodRunnerEnv App+ let app =+ urlToDispatch+ (WithParentArgs (1 :: Int) (Child1R "hello"))+ yre+ req = defaultRequest+ { pathInfo = ["parent", "1", "hello", "child1"]+ , requestMethod = "GET"+ }+ sres <- flip runSession app $ request req+ H.statusCode (simpleStatus sres) `shouldBe` 200+ simpleBody sres `shouldBe` "1hello"+ it "404s a path whose parent arg disagrees with the request path" $ do+ -- The fragment app is rooted at parent arg 1, but it still parses the+ -- path off the request; a path that doesn't match the fragment 404s.+ yre <- mkYesodRunnerEnv App+ let app =+ urlToDispatch+ (WithParentArgs (1 :: Int) (Child1R "hello"))+ yre+ req = defaultRequest+ { pathInfo = ["parent", "1", "hello", "nope"]+ , requestMethod = "GET"+ }+ sres <- flip runSession app $ request req+ H.statusCode (simpleStatus sres) `shouldBe` 404++ describe "UrlToDispatch (Route site) / Text / String / (Route, params)" $ do+ -- The plain hand-written instances all just forward to yesodDispatch;+ -- pin that they actually dispatch (rather than e.g. always 404). The+ -- url value is ignored by these instances, so we always request /nest.+ let dispatchVia :: UrlToDispatch url App => url -> IO SResponse+ dispatchVia url = do+ yre <- mkYesodRunnerEnv App+ let app = urlToDispatch url yre+ req = defaultRequest { pathInfo = ["nest"], requestMethod = "GET" }+ flip runSession app $ request req+ expectNest sres = do+ H.statusCode (simpleStatus sres) `shouldBe` 200+ simpleBody sres `shouldBe` "getNestIndexR"+ it "UrlToDispatch (Route site)" $+ dispatchVia (HomeR :: Route App) >>= expectNest+ it "UrlToDispatch (Route site, params)" $+ dispatchVia (HomeR :: Route App, [] :: [(Text, Text)]) >>= expectNest+ it "UrlToDispatch Text" $+ dispatchVia ("ignored" :: Text) >>= expectNest+ it "UrlToDispatch String" $+ dispatchVia ("ignored" :: String) >>= expectNest++ describe "RedirectUrl (WithParentArgs url) Location header" $ do+ -- Pins toTextUrl . toParentRoute: a handler redirecting via a fragment+ -- + parent args must render the same Location as the full route would.+ it "renders the full nested route as the Location" $ do+ app <- toWaiApp App+ sres <- flip runSession app $+ request defaultRequest+ { pathInfo = ["redirectparent"]+ , requestMethod = "GET"+ , httpVersion = H.http11+ }+ H.statusCode (simpleStatus sres) `shouldBe` 303+ lookup "Location" (simpleHeaders sres)+ `shouldBe` Just "/parent/1/hello/child1"++ describe "RedirectUrl Text / String / (Route, params) Location header" $ do+ -- The unexercised simple RedirectUrl instances, end to end through a+ -- handler redirect.+ it "redirects via plain Text" $ do+ app <- toWaiApp App+ sres <- flip runSession app $+ request defaultRequest+ { pathInfo = ["redirecttext"]+ , requestMethod = "GET"+ , httpVersion = H.http11+ }+ H.statusCode (simpleStatus sres) `shouldBe` 303+ lookup "Location" (simpleHeaders sres)+ `shouldBe` Just "/some/where"+ it "redirects via plain String" $ do+ app <- toWaiApp App+ sres <- flip runSession app $+ request defaultRequest+ { pathInfo = ["redirectstring"]+ , requestMethod = "GET"+ , httpVersion = H.http11+ }+ H.statusCode (simpleStatus sres) `shouldBe` 303+ lookup "Location" (simpleHeaders sres)+ `shouldBe` Just "/some/string"+ it "redirects via (Route, params)" $ do+ app <- toWaiApp App+ sres <- flip runSession app $+ request defaultRequest+ { pathInfo = ["redirectparams"]+ , requestMethod = "GET"+ , httpVersion = H.http11+ }+ H.statusCode (simpleStatus sres) `shouldBe` 303+ lookup "Location" (simpleHeaders sres)+ `shouldBe` Just "/json?q=1"++ describe "RenderRouteNested (WithParentArgs a)" $ do+ it "agrees with renderRoute of the equivalent full Route" $ do+ renderRouteNested () (WithParentArgs (1 :: Int) (Child1R "hello"))+ `shouldBe`+ renderRoute (toParentRoute (1 :: Int) (Child1R "hello"))+ -- and concretely, the full path:+ renderRouteNested () (WithParentArgs (1 :: Int) (Child1R "hello"))+ `shouldBe`+ renderRoute (ParentR 1 (Child1R "hello"))+++ describe "selectRep" $ do+ test "application/json" "JSON"+ test (S8.unpack typeJson) "JSON"+ test "text/xml" "XML"+ test (S8.unpack typeXml) "XML"+ test "text/xml,application/json" "XML"+ test "text/xml;q=0.9,application/json;q=1.0" "JSON"+ test (S8.unpack typeHtml) "HTML"+ test "text/html" "HTML"+ test specialHtml "HTMLSPECIAL"+ testRequest 200 (acceptRequest "application/json") { pathInfo = ["json"] } "{\"message\":\"Invalid Login\"}"+ test "text/*" "HTML"+ test "*/*" "HTML"+ describe "routeAttrs" $ do+ it "HomeR" $ routeAttrs HomeR `shouldBe` Set.singleton "home"+ it "JsonR" $ routeAttrs JsonR `shouldBe` Set.empty+ it "ChildR" $ routeAttrs (ParentR 5 $ Child1R "ignored") `shouldBe` Set.singleton "child"+ -- Bug-2 regression: InnerR is split (RouteAttrsNested InnerR), OuterR is+ -- inlined. The generated clause must carry the OuterR ancestor pattern.+ it "InnerIndexR (split parent under an inlined parent)" $+ routeAttrs (OuterR (InnerR InnerIndexR)) `shouldBe` Set.singleton "innerleaf"++ describe "OuterR (inlined parent delegating to a split InnerR)" $ do+ it "dispatches /outer/inner" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["outer", "inner"]+ , requestMethod = "GET"+ }+ (Just "getInnerIndexR")++ describe "fallthrough" $ do+ it "is 404 because fallthrough is disabled" $ do+ testRequestIO+ 404+ defaultRequest+ { pathInfo = ["nest", "foo"]+ , requestMethod = "GET"+ }+ Nothing++ describe "BlahR" $ do+ -- This test is to verify that you can promote a singleton route-+ -- ie, that+ --+ -- > /blah BlahR GET+ --+ -- and+ --+ -- > /blah BlahR:+ -- > / BlahIndexR GET+ --+ -- are equivalent+ it "can access with just the prefix" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["blah"]+ , requestMethod = "GET"+ }+ (Just "getBlahIndexR")++ it "can access a filename route" $ do+ testRequestIO+ 200+ defaultRequest+ { pathInfo = ["robots.txt"]+ , requestMethod = "GET"+ }+ (Just "robots.txt")++ describe "ToParentRoute" $ do+ describe "Route App" $ do+ it "works with no args" $ do+ toParentRoute () HomeR `shouldBe` HomeR+ it "works with args" $ do+ toParentRoute () (ParentR 1 (Child1R "hello"))+ `shouldBe`+ (ParentR 1 (Child1R "hello"))+ describe "ParentR" $ do+ it "works" $ do+ toParentRoute 3 (Child1R "Hello")+ `shouldBe`+ ParentR 3 (Child1R "Hello")++ describe "Parent0R" $ do+ it "works" $ do+ toParentRoute 3 Parent0IndexR+ `shouldBe`+ Parent0R 3 Parent0IndexR++ describe "Child0R" $ do+ it "works" $ do+ toParentRoute (3, "hello") ParentChildIndexR+ `shouldBe`+ Parent0R 3 (Child0R "hello" ParentChildIndexR)++ it "works with arg" $ do+ toParentRoute (3, "hello") (ParentChildR "asdf")+ `shouldBe`+ Parent0R 3 (Child0R "hello" (ParentChildR "asdf"))
test/YesodCoreTest/NoOverloadedStrings.hs view
@@ -1,7 +1,13 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleInstances, ViewPatterns #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} -- the module name is a lie!!!+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE TypeOperators #-}+ module YesodCoreTest.NoOverloadedStrings ( noOverloadedTest , Widget@@ -14,7 +20,6 @@ import Yesod.Core import Network.Wai import Network.Wai.Test-import Data.Monoid (mempty) import qualified Data.Text as T import qualified Data.ByteString.Lazy.Char8 as L8 @@ -65,7 +70,7 @@ case_sanity :: IO () case_sanity = runner $ do res <- request defaultRequest- assertBody Data.Monoid.mempty res+ assertBody mempty res case_subsite :: IO () case_subsite = runner $ do
test/YesodCoreTest/NoOverloadedStringsSub.hs view
@@ -1,8 +1,12 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} -- hah, the test should be renamed...+{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE OverloadedStrings #-} -- hah, the test should be renamed...+ -- Not actually a problem, we're now requiring overloaded strings, we just need -- to make the docs more explicit about it. module YesodCoreTest.NoOverloadedStringsSub where
+ test/YesodCoreTest/ParamDefaultSplit/Data.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Route data for a delegation-probe regression: a *parameterized* top-level site with a+-- nested route, generated with /default/ opts (i.e. NOT+-- 'setParameterizedSubroute'), so the nested datatype 'SubParentDR' is+-- generated at kind 'Type'. The matching dispatch splice lives in the+-- separately compiled "YesodCoreTest.ParamDefaultSplit.Runtime", so the child+-- datatype is in scope there — the precise shape that made the @go@ delegation+-- probe build the ill-kinded @SubParentDR a@ and abort the splice.+module YesodCoreTest.ParamDefaultSplit.Data where++import Yesod.Core++data PolyD a = PolyD a++mkYesodData "PolyD a" [parseRoutes|+/ HomeDR GET+/item/#Int ItemDR GET+/sub SubParentDR:+ / SubHomeDR GET+ /detail/#Int SubDetailDR GET+|]
+ test/YesodCoreTest/ParamDefaultSplit/Runtime.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Delegation-probe regression: a *parameterized* site (@PolyD a@) with a nested route,+-- using /default/ opts (opted out of nested-route discovery), with the route+-- data and the dispatch splice /split across modules/.+--+-- The dispatch splice's @go@ delegation probe must saturate the in-scope child+-- datatype 'SubParentDR' by its own reified arity (0) — NOT by the site's type+-- args — or it builds the ill-kinded @SubParentDR a@ and @isInstance@ throws a+-- kind error that aborts the splice. So this module compiling at all is the+-- regression assertion; the WAI tests confirm the inlined dispatch still works.+module YesodCoreTest.ParamDefaultSplit.Runtime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core++import YesodCoreTest.ParamDefaultSplit.Data+import YesodCoreTest.RuntimeHarness (assertRequestFor)++mkYesodDispatch "PolyD a" resourcesPolyD++instance Yesod (PolyD a) where+ messageLoggerSource = mempty++getHomeDR :: HandlerFor (PolyD a) Text+getHomeDR = pure "home"++getItemDR :: Int -> HandlerFor (PolyD a) Text+getItemDR _ = pure "item"++getSubHomeDR :: HandlerFor (PolyD a) Text+getSubHomeDR = pure "subHome"++getSubDetailDR :: Int -> HandlerFor (PolyD a) Text+getSubDetailDR _ = pure "subDetail"++sampleRoutes :: [Route (PolyD ())]+sampleRoutes =+ [ HomeDR+ , ItemDR 7+ , SubParentDR SubHomeDR+ , SubParentDR (SubDetailDR 9)+ ]++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor (PolyD ()) method status path mexpected++specs :: Spec+specs = describe "parameterized site, default opts, nested route split across modules" $ do+ describe "parseRoute . renderRoute round-trips" $+ mapM_+ (\r -> it (show r) $ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes++ describe "WAI dispatch (inlined, not delegated)" $ do+ it "static leaf (HomeDR)" $+ testRequestIO 200 [] "GET" (Just "home")+ it "dynamic leaf (ItemDR)" $+ testRequestIO 200 ["item", "7"] "GET" (Just "item")+ it "nested parent home (SubParentDR SubHomeDR)" $+ testRequestIO 200 ["sub"] "GET" (Just "subHome")+ it "nested parent dynamic leaf (SubParentDR (SubDetailDR _))" $+ testRequestIO 200 ["sub", "detail", "9"] "GET" (Just "subDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["sub", "detail", "9", "oops"] "GET" Nothing+ it "405 on wrong method" $+ testRequestIO 405 ["sub"] "POST" Nothing
+ test/YesodCoreTest/ParamFallthroughRuntime.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime coverage for nested-route /fallthrough/ on a *parameterized* site.+-- The monomorphic fallthrough test ("YesodCoreTest.FallthroughDispatch.Runtime")+-- exercises the @setNestedRouteFallthrough@ fallback only for an+-- unparameterized site; this is the parameterized counterpart, opted into+-- nested discovery so the parameterized subroute datatypes carry the type+-- variable while several same-prefix parents fall through to one another.+module YesodCoreTest.ParamFallthroughRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertGet)++data PApp a = PApp a++mkYesodOpts+ (setNestedRouteFallthrough True (setParameterizedSubroute True defaultOpts))+ "PApp a"+ [parseRoutesNoCheck|+/foo FirstFooR:+ / FooIndexR+ /neat FooNeatR++/foo SecondFooR:+ / NeverFiresR+ /other OtherR++/foo/#Int ParamFooR:+ / FooWithIntR++/foo/#String TextFooR:+ / FooWithTextR+|]++instance Yesod (PApp a) where+ messageLoggerSource = mempty++handleFooIndexR :: HandlerFor app String+handleFooIndexR = pure "FooIndexR"++handleOtherR :: HandlerFor app String+handleOtherR = pure "OtherR"++handleFooNeatR :: HandlerFor app String+handleFooNeatR = pure "FooNeatR"++handleNeverFiresR :: HandlerFor app String+handleNeverFiresR = pure "NeverFiresR"++handleFooWithIntR :: Int -> HandlerFor app String+handleFooWithIntR i = pure $ "FooWithIntR " <> show i++handleFooWithTextR :: String -> HandlerFor app String+handleFooWithTextR str = pure $ "FooWithTextR " <> str++testRequestIO :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+testRequestIO = assertGet (PApp ())++specs :: Spec+specs = describe "parameterized site, nested-route fallthrough" $ do+ it "matches the first parent's index (FooIndexR at /foo)" $+ testRequestIO 200 ["foo"] (Just "FooIndexR")+ it "matches a child of the first parent (FooNeatR at /foo/neat)" $+ testRequestIO 200 ["foo", "neat"] (Just "FooNeatR")+ it "falls through to a later same-prefix parent (OtherR at /foo/other)" $+ testRequestIO 200 ["foo", "other"] (Just "OtherR")+ it "falls through to the dynamic-prefix parent (FooWithIntR at /foo/3)" $+ testRequestIO 200 ["foo", "3"] (Just "FooWithIntR 3")+ it "falls through to the textual-prefix parent (FooWithTextR at /foo/asdf)" $+ testRequestIO 200 ["foo", "asdf"] (Just "FooWithTextR asdf")
+ test/YesodCoreTest/ParamFocusSplit/Resources.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Shared route tree + foundation datatype for the parameterized+-- focus-split fixture ("YesodCoreTest.ParamFocusSplit.Runtime" and its+-- @.SubR@ fragment module). The foundation @PApp a@ carries one type+-- argument, so this drives the @TyArgs@-applying focused codegen paths.+module YesodCoreTest.ParamFocusSplit.Resources where++import Yesod.Core+import Yesod.Routes.TH.Types (ResourceTree)++-- | A phantom-parameterized top-level site (kind @Type -> Type@).+data PApp a = PApp++paramFocusResources :: [ResourceTree String]+paramFocusResources = [parseRoutes|+/ HomeR GET+/item/#Int ItemR GET+/sub SubR:+ / SubHomeR GET+ /detail/#Int SubDetailR GET+|]
+ test/YesodCoreTest/ParamFocusSplit/Runtime.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime + round-trip coverage for a *parameterized* top-level site whose+-- nested route block is split into its own module with+-- 'setFocusOnNestedRoute'. Before this fixture the+-- @parameterized + setFocusOnNestedRoute@ corner was never compiled by any+-- test, and the focused 'RouteAttrsNested' instance head was built from a bare+-- constructor (no type argument applied) — a guaranteed kind error for a+-- parameterized site. The parent dispatch is generated here with+-- 'setParameterizedSubroute', so it must *delegate* to the @SubR a@ fragment+-- instances compiled in "YesodCoreTest.ParamFocusSplit.SubR"; the build itself+-- is the split-delegation assertion and the WAI rows confirm dispatch.+module YesodCoreTest.ParamFocusSplit.Runtime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core++import YesodCoreTest.ParamFocusSplit.Resources+-- Brings the split 'SubR' datatype + its nested dispatch / parse / render /+-- attrs instances into scope. Importing the datatype (not @SubR ()@) is what+-- lets the parent splice's delegation probe resolve 'SubR' and delegate to the+-- imported instance instead of regenerating (and demanding) its handlers.+import YesodCoreTest.ParamFocusSplit.SubR (SubR (..))+import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- The parent splice: generates the route data types and the parent dispatch.+-- 'setParameterizedSubroute' forces the NestedDiscovery path so the parent+-- delegates to the split 'SubR' fragment rather than inlining it.+mkYesodOpts (setParameterizedSubroute True defaultOpts) "PApp a" paramFocusResources++instance Yesod (PApp a) where+ messageLoggerSource = mempty++getHomeR :: HandlerFor (PApp a) Text+getHomeR = pure "home"++getItemR :: Int -> HandlerFor (PApp a) Text+getItemR _ = pure "item"++sampleRoutes :: [Route (PApp ())]+sampleRoutes =+ [ HomeR+ , ItemR 7+ , SubR SubHomeR+ , SubR (SubDetailR 9)+ ]++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor (PApp :: PApp ()) method status path mexpected++specs :: Spec+specs = describe "parameterized top-level site, nested routes split with setFocusOnNestedRoute" $ do+ describe "parseRoute . renderRoute round-trips" $+ mapM_+ (\r -> it (show r) $ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes++ describe "WAI dispatch (parent delegates to the split SubR fragment)" $ do+ it "static leaf (HomeR)" $+ testRequestIO 200 [] "GET" (Just "home")+ it "dynamic leaf (ItemR)" $+ testRequestIO 200 ["item", "7"] "GET" (Just "item")+ it "nested parent home (delegated SubR SubHomeR)" $+ testRequestIO 200 ["sub"] "GET" (Just "subHome")+ it "nested parent dynamic leaf (delegated SubR (SubDetailR _))" $+ testRequestIO 200 ["sub", "detail", "9"] "GET" (Just "subDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["sub", "detail", "9", "oops"] "GET" Nothing+ it "405 on wrong method" $+ testRequestIO 405 ["sub"] "POST" Nothing
+ test/YesodCoreTest/ParamFocusSplit/SubR.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | The split-out @SubR@ fragment of the parameterized @PApp a@ site. The+-- focused splice ('setFocusOnNestedRoute') here emits only the @SubR a@+-- fragment's dispatch / parse / render / attrs nested instances; the parent+-- "YesodCoreTest.ParamFocusSplit.Runtime" generates the rest and delegates to these.+-- This module is the compile-time pin for the focused parameterized codegen+-- (the @RouteAttrsNested (SubR a)@ instance head in particular, which used to+-- be built from a bare constructor without the site's type argument).+module YesodCoreTest.ParamFocusSplit.SubR where++import Data.Text (Text)+import Yesod.Core+import YesodCoreTest.ParamFocusSplit.Resources++mkYesodOpts+ (setFocusOnNestedRoute "SubR" (setParameterizedSubroute True defaultOpts))+ "PApp a"+ paramFocusResources++getSubHomeR :: HandlerFor (PApp a) Text+getSubHomeR = pure "subHome"++getSubDetailR :: Int -> HandlerFor (PApp a) Text+getSubDetailR _ = pure "subDetail"
+ test/YesodCoreTest/ParamNestedNoFallthroughRuntime.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Exercises the *delegated* (nested-instance) no-fallthrough dispatch path+-- for a 'ResourceParent' child.+--+-- "YesodCoreTest.ParamNoFallthroughRuntime" is two levels deep (parent ->+-- leaf), so the parent's generated @YesodDispatchNested@ instance only ever+-- dispatches to leaf children. This module goes three levels deep+-- (grandparent -> parent -> leaf) under nested discovery (the site carries a+-- type parameter), so the grandparent's nested instance dispatches to the+-- *inner parent* via 'genNestedDispatchClauses' @ResourceParent@ arm — the arm+-- that, with fallthrough disabled, must convert an inner @Nothing@ into a 404+-- commit rather than returning the raw @Maybe@ (which under an outer+-- fallthrough caller would leak through to sibling routes).+module YesodCoreTest.ParamNestedNoFallthroughRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertGet)++data PNNApp a = PNNApp a++-- Three-level nesting, generated WITHOUT setNestedRouteFallthrough. A request+-- that matches the grandparent and inner-parent prefixes but no leaf must+-- commit to a 404 (the inner parent is delegated to via its own nested+-- dispatch instance), never fall through.+mkYesodOpts+ (setParameterizedSubroute True defaultOpts)+ "PNNApp a"+ [parseRoutesNoCheck|+/grand GrandR:+ /inner InnerR:+ /leaf LeafR+ /also AlsoR+|]++instance Yesod (PNNApp a) where+ messageLoggerSource = mempty++handleLeafR :: HandlerFor app String+handleLeafR = pure "LeafR"++handleAlsoR :: HandlerFor app String+handleAlsoR = pure "AlsoR"++testRequestIO :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+testRequestIO = assertGet (PNNApp ())++specs :: Spec+specs = describe "parameterized site, deep nesting, fallthrough DISABLED" $ do+ it "matches a leaf under the inner parent (/grand/inner/leaf)" $+ testRequestIO 200 ["grand", "inner", "leaf"] (Just "LeafR")+ it "matches the inner parent's other leaf (/grand/inner/also)" $+ testRequestIO 200 ["grand", "inner", "also"] (Just "AlsoR")+ it "404s when the grandparent+parent prefix match but no leaf does (/grand/inner/nope)" $+ -- Drives the delegated ResourceParent no-fallthrough arm: GrandR's+ -- nested instance dispatches to InnerR's nested instance, which returns+ -- Nothing for "nope"; that must become a committed 404.+ testRequestIO 404 ["grand", "inner", "nope"] Nothing+ it "404s when only the grandparent prefix matches (/grand/nope)" $+ testRequestIO 404 ["grand", "nope"] Nothing
+ test/YesodCoreTest/ParamNoExplicitArgs.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Regression pin for invoking @mkYesod@ on a *parameterized* foundation+-- /without/ writing its explicit type argument (here @mkYesod "PNoArgs"@ for+-- @data PNoArgs a@). The reified arity is filled with a fresh type variable,+-- and that fill var must travel in the 'TyArgs' handed to 'discoveryMode' /+-- the generators — otherwise the site is misclassified as monomorphic and+-- emits ill-scoped nested instances (a 1.6 -> 1.7 regression). With the fix it+-- stays on the backwards-compatible inline path. The build is the assertion;+-- the WAI rows confirm it still dispatches.+module YesodCoreTest.ParamNoExplicitArgs+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- One type argument, but the splice below names only "PNoArgs" (no @a@).+data PNoArgs a = PNoArgs++-- @CatchAllR@ overlaps @SubParentR@'s prefix (both start with @sub@), so its+-- @!@ turns overlap checking off for it. It is placed AFTER the parent on+-- purpose: under the historical (1.6) inline @parseRoute@ semantics a path that+-- matches the @sub@ prefix but misses every child must COMMIT at the parent and+-- return 'Nothing', NOT fall through to this later, lower-priority sibling. That+-- is exactly what inline @dispatch@ does (404), so @parseRoute@ has to agree.+mkYesod "PNoArgs" [parseRoutes|+/ HomeR GET+/sub SubParentR:+ / SubHomeR GET+!/sub/#Text CatchAllR GET+|]++instance Yesod (PNoArgs a) where+ messageLoggerSource = mempty++getHomeR :: HandlerFor (PNoArgs a) Text+getHomeR = pure "home"++getSubHomeR :: HandlerFor (PNoArgs a) Text+getSubHomeR = pure "subHome"++getCatchAllR :: Text -> HandlerFor (PNoArgs a) Text+getCatchAllR _ = pure "catchAll"++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor (PNoArgs :: PNoArgs ()) method status path mexpected++specs :: Spec+specs = describe "parameterized foundation invoked without explicit type args" $ do+ it "static leaf dispatches" $+ testRequestIO 200 [] "GET" (Just "home")+ it "inlined nested leaf dispatches" $+ testRequestIO 200 ["sub"] "GET" (Just "subHome")+ it "404 on unknown nested suffix (dispatch commits at the parent prefix)" $+ testRequestIO 404 ["sub", "oops"] "GET" Nothing+ it "parseRoute commits at the parent prefix: a child miss is Nothing, not the later overlapping sibling" $+ -- The regression: with overlap checking off the flat inline codegen let+ -- this fall through to @CatchAllR@, so parseRoute returned @Just+ -- (CatchAllR "oops")@ while dispatch returned 404 — they disagreed for+ -- the same request. Commit-on-parent-prefix makes parseRoute agree.+ (parseRoute (["sub", "oops"], []) :: Maybe (Route (PNoArgs ())))+ `shouldBe` Nothing+ it "parseRoute still reaches a matching child under the parent prefix" $+ (parseRoute (["sub"], []) :: Maybe (Route (PNoArgs ())))+ `shouldBe` Just (SubParentR SubHomeR)
+ test/YesodCoreTest/ParamNoFallthroughRuntime.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | The negative counterpart to "YesodCoreTest.ParamFallthroughRuntime".+--+-- That module exercises nested-route fallthrough with the flag /enabled/ —+-- every assertion expects fallthrough to succeed. This module builds the same+-- overlapping-@\/foo@ parents *without* 'setNestedRouteFallthrough', and+-- asserts a 404 exactly where the enabled version falls through to a later+-- same-prefix parent. Without this, a regression that made nested fallthrough+-- always-on (ignoring 'roNestedRouteFallthrough') would pass every test.+module YesodCoreTest.ParamNoFallthroughRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertGet)++data PNApp a = PNApp a++-- Same routes as ParamFallthroughRuntime's first two parents, but generated+-- WITHOUT setNestedRouteFallthrough: the first parent that matches the @/foo@+-- prefix is committed to, so a path its children can't match 404s rather than+-- falling through to the next @/foo@ parent.+mkYesodOpts+ (setParameterizedSubroute True defaultOpts)+ "PNApp a"+ [parseRoutesNoCheck|+/foo FirstFooR:+ / FooIndexR+ /neat FooNeatR++/foo SecondFooR:+ / NeverFiresR+ /other OtherR+|]++instance Yesod (PNApp a) where+ messageLoggerSource = mempty++handleFooIndexR :: HandlerFor app String+handleFooIndexR = pure "FooIndexR"++handleFooNeatR :: HandlerFor app String+handleFooNeatR = pure "FooNeatR"++handleNeverFiresR :: HandlerFor app String+handleNeverFiresR = pure "NeverFiresR"++handleOtherR :: HandlerFor app String+handleOtherR = pure "OtherR"++testRequestIO :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+testRequestIO = assertGet (PNApp ())++specs :: Spec+specs = describe "parameterized site, nested-route fallthrough DISABLED" $ do+ it "still matches the first parent's index (FooIndexR at /foo)" $+ testRequestIO 200 ["foo"] (Just "FooIndexR")+ it "still matches a child of the first parent (FooNeatR at /foo/neat)" $+ testRequestIO 200 ["foo", "neat"] (Just "FooNeatR")+ it "404s instead of falling through to a later same-prefix parent (/foo/other)" $+ -- With fallthrough this is OtherR (200); without it, the first /foo+ -- parent commits and its children don't match "other".+ testRequestIO 404 ["foo", "other"] Nothing
+ test/YesodCoreTest/ParamSubsite/Data.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}++-- | Opt-in counterpart to "YesodCoreTest.ParameterizedSubData": this subsite+-- enables parameterized subroutes via+-- @'mkYesodSubDataOpts' ('setParameterizedSubroute' 'True' ...)@. With the+-- flag on, the nested subroute datatype 'PNestedR' carries the subsite's type+-- parameter (kind @Type -> Type@), so the threaded constraints / associated+-- type families stay well-scoped. This proves that opting a /subsite/ into+-- parameterized subroutes actually parameterizes its subroutes.+module YesodCoreTest.ParamSubsite.Data where++import Yesod.Core++type RouteConstraints t = (Eq t, Read t, Show t, PathPiece t)++class (RouteConstraints (PAssoc subsite)) => PClass subsite master | subsite -> master where+ type PAssoc subsite+ pValue :: subsite -> master -> String++newtype PSub subsite = PSub subsite++mkYesodSubDataOpts (setParameterizedSubroute True defaultOpts)+ "(PClass subsite master) => PSub subsite" [parseRoutes|+/ PHomeR GET+/item/#Int PItemR GET+!/#{PAssoc subsite}/nested PNestedR:+ / PNestedHomeR GET+ /detail/#Int PNestedDetailR GET+|]++-- | Opt-in kind guard: with parameterized subroutes enabled, the nested+-- subroute datatype is parameterized over the subsite. This signature only+-- compiles if 'PNestedR' has kind @Type -> Type@.+_pNestedRParamGuard :: PNestedR subsite -> PNestedR subsite+_pNestedRParamGuard = id
+ test/YesodCoreTest/ParamSubsite/InstanceRuntime.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime coverage for 'mkYesodSubDispatchInstance' (the public convenience+-- that generates both the @YesodSubDispatch@ instance and the+-- @YesodSubDispatchNested@ instances for a parameterized subsite). The+-- function had **no** in-repo test before; this exercises the happy path —+-- a parameterized subsite whose subroutes are parameterized+-- ('setParameterizedSubroute') — end to end (compile + WAI dispatch + route+-- round-trip).+--+-- The misuse case (parameterized subsite + unparameterized nested datatype)+-- is a compile error by design, so it is covered by the pure unit test for+-- 'checkNestedSubArity' in @Route.SubDispatchAritySpec@ rather than here.+module YesodCoreTest.ParamSubsite.InstanceRuntime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core+import Yesod.Core.Dispatch (toWaiApp, mkYesodSubDispatchInstance)++import YesodCoreTest.ParamSubsite.Data+import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- | Concrete instantiation of the parameterized subsite.+data ConcretePSub = ConcretePSub++instance PClass ConcretePSub App where+ type PAssoc ConcretePSub = Text+ pValue _ _ = "value"++type ConcretePSubT = PSub ConcretePSub++data App = App++-- Handlers for the subsite. The nested handlers receive the parent's+-- 'PAssoc' dynamic, exactly as for any other nested route.+getPHomeR :: SubHandlerFor (PSub subsite) master Text+getPHomeR = pure "pHome"++getPItemR :: Int -> SubHandlerFor (PSub subsite) master Text+getPItemR _ = pure "pItem"++getPNestedHomeR :: PAssoc subsite -> SubHandlerFor (PSub subsite) master Text+getPNestedHomeR _ = pure "pNestedHome"++getPNestedDetailR :: PAssoc subsite -> Int -> SubHandlerFor (PSub subsite) master Text+getPNestedDetailR _ _ = pure "pNestedDetail"++-- The API under test: generate the YesodSubDispatch + YesodSubDispatchNested+-- instances for the parameterized subsite. Placed before 'mkYesod' so the+-- master dispatch can see the generated subsite instances.+mkYesodSubDispatchInstance "(PClass subsite master) => PSub subsite" resourcesPSub++mkYesod "App" [parseRoutes|+/ HomeR GET+/p PSubR ConcretePSubT getP+|]++instance Yesod App where+ messageLoggerSource = mempty++getHomeR :: Handler Text+getHomeR = pure "home"++getP :: App -> ConcretePSubT+getP _ = PSub ConcretePSub++sampleRoutes :: [Route ConcretePSubT]+sampleRoutes =+ [ PHomeR+ , PItemR 7+ , PNestedR "abc" PNestedHomeR+ , PNestedR "abc" (PNestedDetailR 9)+ ]++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor App method status path mexpected++specs :: Spec+specs = describe "mkYesodSubDispatchInstance (parameterized subsite, opt-in subroutes)" $ do+ describe "parseRoute . renderRoute round-trips" $+ mapM_+ (\r -> it (show r) $ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes++ describe "WAI dispatch (subsite mounted at /p)" $ do+ it "static leaf (PHomeR)" $+ testRequestIO 200 ["p"] "GET" (Just "pHome")+ it "dynamic leaf (PItemR)" $+ testRequestIO 200 ["p", "item", "7"] "GET" (Just "pItem")+ it "nested parent home (PNestedR _ PNestedHomeR)" $+ testRequestIO 200 ["p", "abc", "nested"] "GET" (Just "pNestedHome")+ it "nested parent dynamic leaf (PNestedR _ (PNestedDetailR _))" $+ testRequestIO 200 ["p", "abc", "nested", "detail", "9"] "GET" (Just "pNestedDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["p", "abc", "nested", "oops"] "GET" Nothing+ it "405 on wrong method" $+ testRequestIO 405 ["p", "abc", "nested"] "POST" Nothing
+ test/YesodCoreTest/ParamSubsite/SplitNested.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Split-out dispatch module for the *parameterized* subsite's nested+-- 'PNestedR' fragment. This is the parameterized counterpart to+-- "YesodCoreTest.SplitSubsite.NestedR": the subsite carries a type parameter,+-- so the subroute datatype 'PNestedR' is itself parameterized (kind+-- @Type -> Type@), and the subsite's type argument must be threaded through+-- 'mkNestedSubDispatchInstance' as 'SomeTyArgs'.+--+-- Crucially, the nested handlers ('getPNestedHomeR', 'getPNestedDetailR') live+-- ONLY here, not in the module that defines the parent 'YesodSubDispatch'+-- instance. So that parent splice can compile only by *delegating* to the+-- 'YesodSubDispatchNested' instance generated here (via 'isInstance') — if it+-- tried to inline the nested routes it would reference handlers not in scope.+-- A successful build is therefore proof that the cross-module split works for+-- a parameterized subsite, exercising the @SomeTyArgs@ / @applyTyArgs@ branches+-- of the dispatch codegen that were previously untested.+module YesodCoreTest.ParamSubsite.SplitNested () where++import Yesod.Core+import Yesod.Core.Dispatch+ (mkNestedSubDispatchInstance, TyArgs(..))+import Language.Haskell.TH (Type(..), mkName)+import Data.List.NonEmpty (NonEmpty(..))+import Data.Text (Text)++import YesodCoreTest.ParamSubsite.Data++getPNestedHomeR :: PAssoc subsite -> SubHandlerFor (PSub subsite) master Text+getPNestedHomeR _ = pure "pNestedHome"++getPNestedDetailR :: PAssoc subsite -> Int -> SubHandlerFor (PSub subsite) master Text+getPNestedDetailR _ _ = pure "pNestedDetail"++-- Generate @YesodSubDispatchNested (PNestedR subsite)@ in this separate module,+-- with the subsite's type argument applied.+--+-- The 'Cxt' and 'TyArgs' are built manually (rather than parsed from a string+-- like @"(PClass subsite master) => PSub subsite"@) because structured+-- arguments are 'mkNestedSubDispatchInstance''s interface: the string parser+-- belongs to 'mkYesodSubDispatchInstance', the whole-subsite entry point,+-- which does this same construction internally after parsing. A split-out+-- fragment module addresses one nested datatype directly, so it supplies the+-- pieces directly; what is written here is exactly what the parent entry+-- point would thread through for that name string.+$(do+ let subsite = mkName "subsite"+ master = mkName "master"+ cxt = [ ConT (mkName "PClass") `AppT` VarT subsite `AppT` VarT master ]+ tyargs = SomeTyArgs ((VarT subsite, subsite) :| [])+ mkNestedSubDispatchInstance+ (setParameterizedSubroute True defaultOpts)+ "PNestedR"+ cxt+ tyargs+ return+ resourcesPSub)
+ test/YesodCoreTest/ParamSubsite/SplitRuntime.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime coverage for a *parameterized* subsite whose nested routes are+-- /split across modules/ — the combination that had no test before (split was+-- monomorphic-only; parameterized nested-discovery was single-module-only).+--+-- The parent @YesodSubDispatch (PSub subsite) master@ instance is generated+-- here with 'mkYesodSubDispatch', but the nested 'PNestedR' handlers and its+-- 'YesodSubDispatchNested' instance live in the separately compiled+-- "YesodCoreTest.ParamSubsite.SplitNested". Because those handlers are NOT in scope+-- here, this module compiles only if the parent splice *delegates* to the+-- external nested instance rather than inlining it — so the build itself is+-- the split-delegation assertion, and the WAI tests confirm it dispatches.+module YesodCoreTest.ParamSubsite.SplitRuntime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core++import YesodCoreTest.ParamSubsite.Data+import YesodCoreTest.ParamSubsite.SplitNested () -- brings the split nested instance into scope+import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- | Concrete instantiation of the parameterized subsite.+data ConcretePSub = ConcretePSub++instance PClass ConcretePSub App where+ type PAssoc ConcretePSub = Text+ pValue _ _ = "value"++type ConcretePSubT = PSub ConcretePSub++data App = App++-- Only the FLAT subsite handlers live here; the nested ones are deliberately+-- absent (see module haddock).+getPHomeR :: SubHandlerFor (PSub subsite) master Text+getPHomeR = pure "pHome"++getPItemR :: Int -> SubHandlerFor (PSub subsite) master Text+getPItemR _ = pure "pItem"++-- The parent dispatch. Generated in THIS module (separately from the nested+-- 'PNestedR' instance), so its 'mkYesodSubDispatch' splice must delegate to the+-- imported 'YesodSubDispatchNested (PNestedR subsite)' instance.+instance PClass subsite master => YesodSubDispatch (PSub subsite) master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesPSub)++mkYesod "App" [parseRoutes|+/ HomeR GET+/p PSubR ConcretePSubT getP+|]++instance Yesod App where+ messageLoggerSource = mempty++getHomeR :: Handler Text+getHomeR = pure "home"++getP :: App -> ConcretePSubT+getP _ = PSub ConcretePSub++sampleRoutes :: [Route ConcretePSubT]+sampleRoutes =+ [ PHomeR+ , PItemR 7+ , PNestedR "abc" PNestedHomeR+ , PNestedR "abc" (PNestedDetailR 9)+ ]++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor App method status path mexpected++specs :: Spec+specs = describe "parameterized subsite, nested routes split across modules" $ do+ describe "parseRoute . renderRoute round-trips" $+ mapM_+ (\r -> it (show r) $ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes++ describe "WAI dispatch (parent delegates to the split nested instance)" $ do+ it "static leaf (PHomeR)" $+ testRequestIO 200 ["p"] "GET" (Just "pHome")+ it "dynamic leaf (PItemR)" $+ testRequestIO 200 ["p", "item", "7"] "GET" (Just "pItem")+ it "nested parent home (delegated PNestedHomeR)" $+ testRequestIO 200 ["p", "abc", "nested"] "GET" (Just "pNestedHome")+ it "nested parent dynamic leaf (delegated PNestedDetailR)" $+ testRequestIO 200 ["p", "abc", "nested", "detail", "9"] "GET" (Just "pNestedDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["p", "abc", "nested", "oops"] "GET" Nothing+ it "405 on wrong method" $+ testRequestIO 405 ["p", "abc", "nested"] "POST" Nothing
+ test/YesodCoreTest/ParamTopLevelRuntime.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime coverage for a *top-level* parameterized site that opts into+-- nested route discovery ('setParameterizedSubroute'). Existing coverage of+-- this combination ("YesodCoreTest.ParameterizedSite.SubRoute") only fires a+-- single request per route and never round-trips or checks error statuses;+-- this exercises the same opt-in top-level path with full+-- @parseRoute . renderRoute@ round-trips plus 404/405 cases.+module YesodCoreTest.ParamTopLevelRuntime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core++import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- A phantom-parameterized top-level site. With 'setParameterizedSubroute'+-- enabled, the nested subroute datatype 'SubParentR' carries the parameter+-- (kind @Type -> Type@), so this drives the NestedDiscovery codegen path.+data Poly a = Poly a++mkYesodOpts (setParameterizedSubroute True defaultOpts) "Poly a" [parseRoutes|+/ HomeR GET+/item/#Int ItemR GET+/sub SubParentR:+ / SubHomeR GET+ /detail/#Int SubDetailR GET+|]++instance Yesod (Poly a) where+ messageLoggerSource = mempty++getHomeR :: HandlerFor (Poly a) Text+getHomeR = pure "home"++getItemR :: Int -> HandlerFor (Poly a) Text+getItemR _ = pure "item"++getSubHomeR :: HandlerFor (Poly a) Text+getSubHomeR = pure "subHome"++getSubDetailR :: Int -> HandlerFor (Poly a) Text+getSubDetailR _ = pure "subDetail"++sampleRoutes :: [Route (Poly ())]+sampleRoutes =+ [ HomeR+ , ItemR 7+ , SubParentR SubHomeR+ , SubParentR (SubDetailR 9)+ ]++testRequestIO+ :: HasCallStack+ => Int+ -> [Text]+ -> H.Method+ -> Maybe L.ByteString+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor (Poly ()) method status path mexpected++specs :: Spec+specs = describe "top-level parameterized site, opted into nested discovery" $ do+ describe "parseRoute . renderRoute round-trips" $+ mapM_+ (\r -> it (show r) $ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes++ describe "WAI dispatch" $ do+ it "static leaf (HomeR)" $+ testRequestIO 200 [] "GET" (Just "home")+ it "dynamic leaf (ItemR)" $+ testRequestIO 200 ["item", "7"] "GET" (Just "item")+ it "nested parent home (SubParentR SubHomeR)" $+ testRequestIO 200 ["sub"] "GET" (Just "subHome")+ it "nested parent dynamic leaf (SubParentR (SubDetailR _))" $+ testRequestIO 200 ["sub", "detail", "9"] "GET" (Just "subDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["sub", "detail", "9", "oops"] "GET" Nothing+ it "405 on wrong method" $+ testRequestIO 405 ["sub"] "POST" Nothing
+ test/YesodCoreTest/ParameterizedSite.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE OverloadedStrings #-}++module YesodCoreTest.ParameterizedSite+ ( parameterizedSiteTest+ ) where++import Data.ByteString.Lazy (ByteString)+import Network.Wai.Test (runSession, request, defaultRequest, assertBodyContains, setPath)+import Test.Hspec (Spec, describe, it)+import Yesod.Core (YesodDispatch, renderRoute, Route)+import Yesod.Core.Dispatch (toWaiApp)+import Data.Traversable+import Data.Foldable+import Data.Text.Encoding+import Data.Text as T++import YesodCoreTest.ParameterizedSite.PolyAny (PolyAny (..))+import YesodCoreTest.ParameterizedSite.PolyShow (PolyShow (..))+import YesodCoreTest.ParameterizedSite.Compat (Compat (..))+import qualified YesodCoreTest.ParameterizedSite.SubRoute as SR++-- These are actually tests for template haskell. So if it compiles, it works+parameterizedSiteTest :: Spec+parameterizedSiteTest = describe "Polymorphic Yesod sites" $ do+ it "Polymorphic unconstrained stub" $ runStub (PolyAny ())+ it "Polymorphic stub with Show" $ runStub' "1337" (PolyShow (1337 :: Int))+ it "Polymorphic unconstrained stub, old-style" $ runStub (Compat () ())+ it "Polymorphic stub, sub routes" $ runStubAgainst [(SR.HomeR 456, ["Stub", "123", "456"]), (SR.EditorR (SR.AwayR 789), ["Stub", "123", "789"])] (SR.SubRoute (123 :: Int))++runStub :: YesodDispatch a => a -> IO ()+runStub stub =+ let actions = do+ res <- request defaultRequest+ assertBodyContains "Stub" res+ in toWaiApp stub >>= runSession actions+++runStub' :: YesodDispatch a => ByteString -> a -> IO ()+runStub' body stub =+ let actions = do+ res <- request defaultRequest+ assertBodyContains "Stub" res+ assertBodyContains body res+ in toWaiApp stub >>= runSession actions++runStubAgainst :: YesodDispatch a => [(Route a, [ByteString])] -> a -> IO ()+runStubAgainst urlBodies stub =+ let actions = do+ for_ urlBodies $ \(url, bodies) -> do+ let (nonQuery, _query) = renderRoute url+ let render = T.intercalate "/" nonQuery+ let req = setPath defaultRequest (encodeUtf8 render)+ res <- request req+ for bodies (`assertBodyContains` res)+ in toWaiApp stub >>= runSession actions
+ test/YesodCoreTest/ParameterizedSite/Compat.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module YesodCoreTest.ParameterizedSite.Compat+ ( Compat (..)+ ) where++import Yesod.Core++-- | Parameterized without constraints, and we call mkYesod without type vars,+-- like people used to do before the last 3 commits+data Compat a b = Compat a b++mkYesod "Compat" [parseRoutes|+/ HomeR GET+|]++instance Yesod (Compat a b)++getHomeR :: Handler a b Html+getHomeR = defaultLayout+ [whamlet|+ <p>+ Stub+ |]
+ test/YesodCoreTest/ParameterizedSite/PolyAny.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module YesodCoreTest.ParameterizedSite.PolyAny+ ( PolyAny (..)+ ) where++import Yesod.Core++-- | Parameterized without constraints+data PolyAny a = PolyAny a++mkYesod "PolyAny a" [parseRoutes|+/ HomeR GET+|]++instance Yesod (PolyAny a)++getHomeR :: Handler a Html+getHomeR = defaultLayout+ [whamlet|+ <p>+ Stub+ |]
+ test/YesodCoreTest/ParameterizedSite/PolyShow.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-unused-top-binds #-}++module YesodCoreTest.ParameterizedSite.PolyShow+ ( PolyShow (..)+ ) where++import Yesod.Core++-- | Parameterized with 'Show' constraint+data PolyShow a = PolyShow a++mkYesod "(Show a) => PolyShow a" [parseRoutes|+/ HomeR GET+|]++instance Show a => Yesod (PolyShow a)++getHomeR :: Show a => Handler a Html+getHomeR = do+ PolyShow x <- getYesod+ defaultLayout+ [whamlet|+ <p>+ Stub #{show x}+ |]
+ test/YesodCoreTest/ParameterizedSite/SubRoute.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE+ TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses+ , OverloadedStrings, StandaloneDeriving, FlexibleInstances, FlexibleContexts+ , ViewPatterns, UndecidableInstances, ConstraintKinds+ #-}+++module YesodCoreTest.ParameterizedSite.SubRoute where++import Yesod.Core+import Data.Kind (Type)++type Constraints p = (Eq (Key p), Show (Key p), Read (Key p), PathPiece (Key p), Show p)++class Constraints p => SiteClass p where+ type Key p :: Type++newtype SubRoute a v = SubRoute a+ deriving (Eq, Show, Read)++instance SiteClass Int where+ type Key Int = Int++mkYesodOpts (setParameterizedSubroute True defaultOpts) "(SiteClass p) => SubRoute p v" [parseRoutes|+/home/#{Key p} HomeR GET+/editor EditorR:+ /away/#{Key p} AwayR GET+|]++{-+The above generates data structures and instances like the following:++ data Route (SubRoute p v) = HomeR (Key p) | EditorR (EditorR p v)++deriving instance SiteClass p => Eq (Route (SubRoute p v))+deriving instance SiteClass p => Show (Route (SubRoute p v))+deriving instance SiteClass p => Read (Route (SubRoute p v))+data EditorR p v = AwayR (Key p)+deriving instance SiteClass p => Eq (EditorR p v)+deriving instance SiteClass p => Show (EditorR p v)+deriving instance SiteClass p => Read (EditorR p v)++Note that `p` is now threaded through the other data structures.++Otherwise, EditorR's definition would've been:+data EditorR+ = AwayR (Key p)+ deriving (Eq, Show, Read)+which clearly doesn't work, as `p` is not in scope.+-}++-- | Opt-in guard (the counterpart to the backwards-compat guard in+-- "YesodCoreTest.ParameterizedSubDispatch"). With 'setParameterizedSubroute'+-- enabled, the nested subroute datatype /does/ carry the parent's type+-- parameters — here 'EditorR' has kind @Type -> Type -> Type@ — so the+-- threaded constraints stay well-scoped. This signature only compiles if the+-- opt-in still parameterizes the subroute, proving the nested-discovery+-- feature remains available behind the flag.+_editorRParamGuard :: EditorR p v -> EditorR p v+_editorRParamGuard = id++instance SiteClass a => Yesod (SubRoute a v)++getHomeR :: SiteClass a => Key a -> HandlerFor (SubRoute a v) Html+getHomeR key = do+ SubRoute x <- liftHandler getYesod+ defaultLayout+ [whamlet|+ <p>+ Stub #{show x} #{show key}+ |]++getAwayR :: SiteClass a => Key a -> HandlerFor (SubRoute a v) Html+getAwayR key1 = do+ SubRoute x <- liftHandler getYesod+ defaultLayout+ [whamlet|+ <p>+ Stub #{show x} #{show key1}+ |]
+ test/YesodCoreTest/ParameterizedSubData.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE FunctionalDependencies #-}+-- ^ Useful for debugging generated code; can be removed once stable++-- | Test case for mkYesodSubData and mkYesodSubDispatch with+-- parameterized/constrained types. Reproduces bugs reported by+-- l0neGamer on PR #1887:+--+-- 1. mkYesodSubData with a constrained type like+-- @"(SomeClass subsite master) => SubsiteData subsite"@+-- where 'master' appears in the context but NOT as a type parameter+-- of the subsite, produces "Out of scope type variable" errors in+-- family instances.+--+-- 2. mkYesodSubDispatch with the same setup produces "Expecting one+-- more argument to 'NestedR'" because isInstance checks and Proxy+-- types weren't fully applied. See ParameterizedSubDispatch.hs.+module YesodCoreTest.ParameterizedSubData where++import Yesod.Core++type RouteConstraints t = (Eq t, Read t, Show t, PathPiece t)++-- | A multi-param type class where 'master' is NOT a parameter of the+-- subsite type, mimicking the pattern:+-- @mkYesodSubData "(SubsiteClass subsite master) => SubsiteData subsite"@+-- The fundep ensures 'master' is determined by 'subsite', which is+-- typical for real-world subsite classes.+class (RouteConstraints (AssocType subsite)) => ParamSubsiteClass subsite master | subsite -> master where+ type AssocType subsite+ getSubsiteValue :: subsite -> master -> String++newtype ParamSubsite subsite = ParamSubsite subsite++mkYesodSubData "(ParamSubsiteClass subsite master) => ParamSubsite subsite" [parseRoutes|+/ ParamSubHomeR GET+/item/#Int ParamSubItemR GET+!/#{AssocType subsite}/nested NestedR:+ / NestedHomeR GET+ /detail/#Int NestedDetailR GET+|]
+ test/YesodCoreTest/ParameterizedSubDispatch.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Test mkYesodSubDispatch with parameterized/constrained types.+-- This exercises the Dispatch TH code path for parameterized subsites.+--+-- Tests two subsites:+-- 1. ParamSubDispatch (from Data submodule) - simple parameterized subsite+-- 2. ParamSubsite (from ParameterizedSubData) - with associated types,+-- dynamic pieces in nested route parents, and ConstraintKinds+module YesodCoreTest.ParameterizedSubDispatch where++import Yesod.Core+import Data.Text (Text)+import YesodCoreTest.ParameterizedSubDispatch.Data+import YesodCoreTest.ParameterizedSubData++-- Handlers for ParamSubDispatch (simple parameterized subsite)+getParamDispHomeR :: SubHandlerFor (ParamSubDispatch subsite) master Text+getParamDispHomeR = pure "home"++getParamDispItemR :: Int -> SubHandlerFor (ParamSubDispatch subsite) master Text+getParamDispItemR _ = pure "item"++instance ParamSubDispatchClass subsite master => YesodSubDispatch (ParamSubDispatch subsite) master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesParamSubDispatch)++-- Handlers for ParamSubsite (with associated types and nested routes)+getParamSubHomeR :: SubHandlerFor (ParamSubsite subsite) master Text+getParamSubHomeR = pure "paramSubHome"++getParamSubItemR :: Int -> SubHandlerFor (ParamSubsite subsite) master Text+getParamSubItemR _ = pure "paramSubItem"++getNestedHomeR :: AssocType subsite -> SubHandlerFor (ParamSubsite subsite) master Text+getNestedHomeR _ = pure "nestedHome"++getNestedDetailR :: AssocType subsite -> Int -> SubHandlerFor (ParamSubsite subsite) master Text+getNestedDetailR _ _ = pure "nestedDetail"++instance+ ( ParamSubsiteClass subsite master+ ) => YesodSubDispatch (ParamSubsite subsite) master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesParamSubsite)++-- | Backwards-compatibility guard for the reported regression.+--+-- By default ('defaultOpts', as used by 'mkYesodSubData'), nested subroute+-- datatypes must remain *unparameterized* — kind 'Type' — exactly as+-- pre-nested-discovery Yesod generated them. The reported breakage was that+-- the branch silently gave them the parent's type parameter (kind+-- @Type -> Type@), which broke any downstream type signature mentioning the+-- subroute.+--+-- This signature only compiles if 'NestedR' has kind 'Type'. If the default+-- ever regresses to parameterizing subroutes, 'NestedR' would have kind+-- @Type -> Type@ and this would fail with a kind error — turning the+-- downstream breakage into a local, in-repo test failure.+_nestedRArityGuard :: NestedR -> NestedR+_nestedRArityGuard = id
+ test/YesodCoreTest/ParameterizedSubDispatch/Data.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ViewPatterns #-}++-- | Route data for a parameterized subsite used by+-- ParameterizedSubDispatch to test mkYesodSubDispatch.+module YesodCoreTest.ParameterizedSubDispatch.Data where++import Yesod.Core++class ParamSubDispatchClass subsite master | subsite -> master where+ getDispatchValue :: subsite -> master -> String++data ParamSubDispatch subsite = ParamSubDispatch subsite++mkYesodSubData "(ParamSubDispatchClass subsite master) => ParamSubDispatch subsite" [parseRoutes|+/ ParamDispHomeR GET+/item/#Int ParamDispItemR GET+|]
+ test/YesodCoreTest/ParameterizedSubDispatchRuntime.hs view
@@ -0,0 +1,181 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime coverage for the backwards-compatible (opt-out) inline+-- nested-route codegen.+--+-- The modules "YesodCoreTest.ParameterizedSubData" /+-- "YesodCoreTest.ParameterizedSubDispatch" only exercise the parameterized+-- /opt-out/ path at *compile time* (data + dispatch instances, no runtime+-- assertions). This module gives the same shape a concrete instantiation and+-- actually exercises it at runtime:+--+-- * 'parseRoute' . 'renderRoute' round-trips for every constructor, and+-- * WAI dispatch returns the right status + body for each path,+--+-- covering the static, dynamic, and (dynamically-keyed) nested-parent inline+-- arms — the path-piece ordering, parent-constructor nesting and @dyns@+-- handling that previously had no runtime guard.+module YesodCoreTest.ParameterizedSubDispatchRuntime+ ( specs+ ) where++import Data.Text (Text)+import Test.Hspec+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Types as H+import Yesod.Core+import Yesod.Core.Dispatch (toWaiApp)++import YesodCoreTest.ParameterizedSubData+import YesodCoreTest.ParameterizedSubDispatch ()+import YesodCoreTest.ParameterizedSubDispatchRuntime.Data+import YesodCoreTest.RuntimeHarness (assertRequestFor)++-- | A concrete subsite for 'ParamSubsite'. Its associated 'AssocType' is+-- 'Text', so the dynamically-keyed nested parent (@!/#{AssocType subsite}@)+-- becomes a plain @Text@ path piece at runtime.+data ConcreteSub = ConcreteSub++instance ParamSubsiteClass ConcreteSub App where+ type AssocType ConcreteSub = Text+ getSubsiteValue _ _ = "concrete"++-- | parseRoutes can only embed a single-token subsite type, so we alias the+-- fully-applied subsites here.+type ConcreteParamSub = ParamSubsite ConcreteSub+type ConcreteBigSub = BigSub ()++data App = App++mkYesod "App" [parseRoutes|+/ HomeR GET+/sub ParamSubR ConcreteParamSub getSub+/big BigSubR ConcreteBigSub getBig+|]++instance Yesod App where+ messageLoggerSource = mempty++getHomeR :: Handler Text+getHomeR = pure "home"++getSub :: App -> ConcreteParamSub+getSub _ = ParamSubsite ConcreteSub++getBig :: App -> ConcreteBigSub+getBig _ = BigSub ()++-- Dispatch + handlers for the BigSub / ChildSub subsites.+instance YesodSubDispatch (BigSub a) master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesBigSub)++instance YesodSubDispatch ChildSub master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesChildSub)++getBigItemR :: Int -> SubHandlerFor (BigSub a) master Text+getBigItemR _ = pure "bigItem"++getBigMultiR :: [Text] -> SubHandlerFor (BigSub a) master Text+getBigMultiR _ = pure "bigMulti"++getWrapHomeR :: Int -> SubHandlerFor (BigSub a) master Text+getWrapHomeR _ = pure "wrapHome"++getWrapDetailR :: Int -> Int -> SubHandlerFor (BigSub a) master Text+getWrapDetailR _ _ = pure "wrapDetail"++getChildHomeR :: SubHandlerFor ChildSub master Text+getChildHomeR = pure "childHome"++getChildXR :: Int -> SubHandlerFor ChildSub master Text+getChildXR _ = pure "childX"++-- | Every constructor of the parameterized subsite's route, instantiated at+-- the concrete subsite.+sampleRoutes :: [Route ConcreteParamSub]+sampleRoutes =+ [ ParamSubHomeR+ , ParamSubItemR 7+ , NestedR "abc" NestedHomeR+ , NestedR "abc" (NestedDetailR 9)+ ]++-- | Routes for 'BigSub', covering a multipiece leaf and a subsite leaf nested+-- under a 'ResourceParent'.+bigSampleRoutes :: [Route ConcreteBigSub]+bigSampleRoutes =+ [ BigItemR 3+ , BigMultiR ["a", "b", "c"]+ , BigMultiR []+ , WrapR 5 WrapHomeR+ , WrapR 5 (WrapDetailR 9)+ , EmbedParentR (ChildR ChildHomeR)+ , EmbedParentR (ChildR (ChildXR 9))+ ]++testRequestIO+ :: HasCallStack+ => Int -- ^ expected http status code+ -> [Text] -- ^ pathInfo+ -> H.Method -- ^ request method+ -> Maybe L.ByteString -- ^ expected body (Nothing = don't check)+ -> IO ()+testRequestIO status path method mexpected =+ assertRequestFor App method status path mexpected++specs :: Spec+specs = describe "opt-out (backwards-compat) parameterized nested routes" $ do+ describe "parseRoute . renderRoute round-trips" $ do+ mapM_+ (\r -> it (show r) $+ parseRoute (renderRoute r) `shouldBe` Just r)+ sampleRoutes+ mapM_+ (\r -> it (show r) $+ parseRoute (renderRoute r) `shouldBe` Just r)+ bigSampleRoutes++ describe "WAI dispatch (subsite mounted at /sub)" $ do+ it "static leaf (ParamSubHomeR)" $+ testRequestIO 200 ["sub"] "GET" (Just "paramSubHome")+ it "dynamic leaf (ParamSubItemR)" $+ testRequestIO 200 ["sub", "item", "7"] "GET" (Just "paramSubItem")+ it "nested parent home (NestedR _ NestedHomeR)" $+ testRequestIO 200 ["sub", "abc", "nested"] "GET" (Just "nestedHome")+ it "nested parent dynamic leaf (NestedR _ (NestedDetailR _))" $+ testRequestIO 200 ["sub", "abc", "nested", "detail", "9"] "GET" (Just "nestedDetail")+ it "404 on unknown nested suffix" $+ testRequestIO 404 ["sub", "abc", "nested", "oops"] "GET" Nothing+ it "405 on wrong method for nested leaf" $+ testRequestIO 405 ["sub", "abc", "nested"] "POST" Nothing+ it "404 on malformed dynamic leaf piece (#Int given non-integer)" $+ testRequestIO 404 ["sub", "item", "notanint"] "GET" Nothing+ it "404 on malformed dynamic nested-leaf piece (NestedDetailR #Int)" $+ testRequestIO 404 ["sub", "abc", "nested", "detail", "notanint"] "GET" Nothing++ describe "WAI dispatch (BigSub mounted at /big)" $ do+ it "dynamic leaf (BigItemR)" $+ testRequestIO 200 ["big", "item", "3"] "GET" (Just "bigItem")+ it "multipiece leaf (BigMultiR)" $+ testRequestIO 200 ["big", "multi", "a", "b", "c"] "GET" (Just "bigMulti")+ it "multipiece leaf, empty tail (BigMultiR [])" $+ testRequestIO 200 ["big", "multi"] "GET" (Just "bigMulti")+ it "nested parent home (WrapR _ WrapHomeR)" $+ testRequestIO 200 ["big", "wrap", "5"] "GET" (Just "wrapHome")+ it "nested parent dynamic leaf (WrapR _ (WrapDetailR _))" $+ testRequestIO 200 ["big", "wrap", "5", "detail", "9"] "GET" (Just "wrapDetail")+ it "subsite leaf nested under parent, home (EmbedParentR (ChildR ChildHomeR))" $+ testRequestIO 200 ["big", "embed", "sub"] "GET" (Just "childHome")+ it "subsite leaf nested under parent, dynamic (EmbedParentR (ChildR (ChildXR _)))" $+ testRequestIO 200 ["big", "embed", "sub", "x", "9"] "GET" (Just "childX")+ it "404 on malformed dynamic parent key (WrapR #Int given non-integer)" $+ testRequestIO 404 ["big", "wrap", "notanint"] "GET" Nothing
+ test/YesodCoreTest/ParameterizedSubDispatchRuntime/Data.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Route data for the runtime opt-out coverage in+-- "YesodCoreTest.ParameterizedSubDispatchRuntime". Kept in its own module so+-- the 'mkYesodSubDispatch' splices there can refer to the @resources*@ values+-- generated here (TH stage restriction).+module YesodCoreTest.ParameterizedSubDispatchRuntime.Data where++import Yesod.Core+import Data.Text (Text)++-- | A monomorphic leaf subsite, embedded under a 'ResourceParent' of 'BigSub'+-- to exercise the inline @Subsite@ arm+-- ('generateParseRouteClausesInline' in Yesod.Routes.TH.ParseRoute).+data ChildSub = ChildSub++mkYesodSubData "ChildSub" [parseRoutes|+/ ChildHomeR GET+/x/#Int ChildXR GET+|]++-- | A parameterized (hence opt-out) subsite that covers a multipiece leaf and+-- a subsite leaf nested under a 'ResourceParent'. The type parameter @a@ is+-- phantom — its only job is to make @tyargs@ non-empty so the+-- backwards-compatible inline path is taken.+newtype BigSub a = BigSub a++mkYesodSubData "BigSub a" [parseRoutes|+/item/#Int BigItemR GET+/multi/+[Text] BigMultiR GET+/wrap/#Int WrapR:+ / WrapHomeR GET+ /detail/#Int WrapDetailR GET+/embed EmbedParentR:+ /sub ChildR ChildSub getChildSub+|]++getChildSub :: BigSub a -> ChildSub+getChildSub _ = ChildSub
test/YesodCoreTest/RawResponse.hs view
@@ -1,4 +1,10 @@-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses, ScopedTypeVariables #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.RawResponse ( specs , Widget
test/YesodCoreTest/Redirect.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE QuasiQuotes, TemplateHaskell, TypeFamilies, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.Redirect ( specs , Widget
+ test/YesodCoreTest/RenderRouteSpec.hs view
@@ -0,0 +1,51 @@+{-# language CPP #-}+{-# language TemplateHaskell #-}+{-# language TypeApplications #-}+{-# language OverloadedStrings #-}+{-# language TypeFamilies #-}+{-# language ViewPatterns #-}++-- | Behavioural @renderRoute@ spec built from a hand-constructed+-- 'ResourceTree' AST (a @ResourceParent@ with one dynamic piece over a leaf)+-- rather than @parseRoutes@, so it pins @mkRenderRouteInstanceOpts@ directly+-- against a known tree. Companion module "YesodCoreTest.RenderRouteSpec.TH"+-- reuses the generated instance from a compile-time splice.+module YesodCoreTest.RenderRouteSpec where++import Yesod.Routes.TH.Types+import Language.Haskell.TH+import Yesod.Routes.TH.RenderRoute+import Yesod.Routes.Class+import Test.Hspec++data App = App++-- Generate the 'RenderRoute' instance for 'App' at compile time, then check+-- 'renderRoute' behaves at runtime (the prod path; the intermediate clause+-- lists are not asserted directly).+do+ let int = ConT ''Int+ mkRenderRouteInstanceOpts defaultOpts [] NoTyArgs (ConT (mkName "App"))+ [ ResourceParent "FirstR" False mempty [Static "first", Dynamic int]+ [ ResourceLeaf Resource+ { resourceName = "BlahR"+ , resourcePieces = [Static "blah"]+ , resourceDispatch = Methods+ { methodsMulti = Nothing+ , methodsMethods = []+ }+ , resourceAttrs = []+ , resourceCheck = False+ }+ ]+ ]++spec :: Spec+spec = do+ describe "renderRoute" $ do+ it "works on top level" $ do+ case renderRoute (FirstR 1 BlahR) of+ (["first", "1", "blah"], []) ->+ pure @IO ()+ wrong ->+ fail $ "Expected renderRoute to work, but got: " <> show wrong
+ test/YesodCoreTest/RenderRouteSpec/TH.hs view
@@ -0,0 +1,32 @@+{-# language TemplateHaskell #-}+{-# language TypeApplications #-}+{-# language OverloadedStrings #-}+{-# language TypeFamilies #-}+{-# language ViewPatterns #-}++-- | Compile-time companion to "YesodCoreTest.RenderRouteSpec": reuses that+-- module's generated @RenderRoute App@ instance and @fail@s the splice unless+-- both @renderRoute@ (full route) and @renderRouteNested@ (from the leaf, given+-- the parent's captured arg) reconstruct the same path. Lives apart from the+-- generating module so the instance is in scope when these top-level splices run.+module YesodCoreTest.RenderRouteSpec.TH where++import Yesod.Routes.Class+import YesodCoreTest.RenderRouteSpec++do+ case renderRoute (FirstR 1 BlahR) of+ (["first", "1", "blah"], []) ->+ pure ()+ wrong ->+ fail $ "Expecte renderRoute to work, but got: " <> show wrong++ pure []++do+ case renderRouteNested 1 BlahR of+ (["first", "1", "blah"], []) ->+ pure []+ wrong ->+ fail $ "Expected renderRouteNested to work, but got: " <> show wrong+
test/YesodCoreTest/Reps.hs view
@@ -1,4 +1,10 @@-{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, TypeFamilies, MultiParamTypeClasses, ViewPatterns #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.Reps ( specs , Widget
test/YesodCoreTest/RequestBodySize.hs view
@@ -1,6 +1,10 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.RequestBodySize ( specs , Widget
+ test/YesodCoreTest/RuntimeHarness.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++-- | A single WAI round-trip helper shared by the parameterized / nested+-- dispatch runtime specs. Previously each of those modules carried its own+-- near-identical @testRequestIO@, in two disagreeing styles+-- (@shouldBe@-on-@simpleStatus@ vs. @assertStatus@). This is the one shared+-- harness: it takes the action that builds the WAI app plus the request+-- details, and asserts the response status (and body, when given) using+-- 'assertStatus' / 'assertBody' so a mismatch prints the offending response.+module YesodCoreTest.RuntimeHarness+ ( assertRequest+ , assertRequestFor+ , assertGet+ , assertRequestRaw+ ) where++import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import Network.Wai (Application, Request, pathInfo, requestMethod)+import Network.Wai.Test+import Test.Hspec (HasCallStack)+import Yesod.Core (Yesod, YesodDispatch, toWaiApp)+import qualified Network.HTTP.Types as H++-- | Build the WAI app, issue one request at @path@ with @method@, assert the+-- response status, and (when given) assert the body.+assertRequest+ :: HasCallStack+ => IO Application -- ^ build the WAI app (e.g. @toWaiApp App@)+ -> H.Method -- ^ request method+ -> Int -- ^ expected status code+ -> [Text] -- ^ request path+ -> Maybe ByteString -- ^ expected body, if checked+ -> IO ()+assertRequest mkApp method status path mexpected = do+ app <- mkApp+ flip runSession app $ do+ sres <- request defaultRequest+ { pathInfo = path+ , requestMethod = method+ }+ assertStatus status sres+ case mexpected of+ Nothing -> pure ()+ Just expected -> assertBody expected sres++-- | 'assertRequest' specialised to a concrete site: builds the WAI app with+-- 'toWaiApp' for you. This is the shape almost every runtime spec wants, and+-- standardises the argument order (@site → method → status → path → body@) that+-- the per-module @testRequestIO@ wrappers used to spell inconsistently.+assertRequestFor+ :: (HasCallStack, Yesod site, YesodDispatch site)+ => site -- ^ the site to dispatch against+ -> H.Method -- ^ request method+ -> Int -- ^ expected status code+ -> [Text] -- ^ request path+ -> Maybe ByteString -- ^ expected body, if checked+ -> IO ()+assertRequestFor site = assertRequest (toWaiApp site)++-- | 'assertRequestFor' fixed to @GET@ — the common case.+assertGet+ :: (HasCallStack, Yesod site, YesodDispatch site)+ => site -> Int -> [Text] -> Maybe ByteString -> IO ()+assertGet site = assertRequestFor site "GET"++-- | Like 'assertRequest', but issue a fully caller-built 'Request' (e.g. one+-- carrying an @Accept@ header for content negotiation) rather than just a+-- path + method. 'assertStatus'/'assertBody' already print the offending+-- response on a mismatch, so callers don't need their own annotate-on-failure+-- wrapper.+assertRequestRaw+ :: HasCallStack+ => IO Application -> Request -> Int -> Maybe ByteString -> IO ()+assertRequestRaw mkApp req status mexpected = do+ app <- mkApp+ flip runSession app $ do+ sres <- request req+ assertStatus status sres+ case mexpected of+ Nothing -> pure ()+ Just expected -> assertBody expected sres
+ test/YesodCoreTest/SplitSubsite/Data.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Data module for the split-subsite test. Defines a (non-parameterized)+-- subsite with a nested route, and generates its route datatype, render and+-- parse instances. Because the subsite has no type parameters, the+-- nested-discovery machinery (RenderRouteNested / ParseRouteNested for the+-- nested 'NestedR' fragment) is generated here, so that a separately compiled+-- module can supply the matching 'YesodSubDispatchNested' instance.+module YesodCoreTest.SplitSubsite.Data where++import Yesod.Core++data SplitSub = SplitSub++mkYesodSubData "SplitSub" [parseRoutes|+/ SplitHomeR GET+/nested NestedR:+ / NestedHomeR GET+ /detail/#Int NestedDetailR GET+|]
+ test/YesodCoreTest/SplitSubsite/NestedR.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | The split-out dispatch module for the subsite's nested 'NestedR'+-- fragment. The handlers for the nested routes live here, alongside the+-- 'YesodSubDispatchNested' instance generated by 'mkNestedSubDispatchInstance'.+--+-- Crucially, this module is compiled /separately/ from the module that+-- defines the subsite's top-level 'YesodSubDispatch' instance. That instance's+-- 'mkYesodSubDispatch' splice sees this 'YesodSubDispatchNested' instance via+-- 'isInstance' and delegates to it — exactly the cross-module splitting that+-- "YesodCoreTest.NestedDispatch.Runtime" demonstrates for top-level sites.+module YesodCoreTest.SplitSubsite.NestedR (NestedR(..)) where++import Yesod.Core+import Yesod.Core.Dispatch+ (mkNestedSubDispatchInstance, defaultOpts, TyArgs(..))+import Data.Text (Text, pack)++import YesodCoreTest.SplitSubsite.Data++getNestedHomeR :: SubHandlerFor SplitSub master Text+getNestedHomeR = pure "nestedHome"++getNestedDetailR :: Int -> SubHandlerFor SplitSub master Text+getNestedDetailR n = pure ("nestedDetail:" <> tshow n)+ where+ tshow = pack . show++$(mkNestedSubDispatchInstance+ defaultOpts+ "NestedR"+ [] -- no instance context+ NoTyArgs -- no type arguments (non-parameterized subsite)+ return -- handler unwrapper+ resourcesSplitSub)
+ test/YesodCoreTest/SplitSubsite/Runtime.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | End-to-end test that a subsite's nested routes can be split across+-- modules. The nested dispatch + handlers live in+-- "YesodCoreTest.SplitSubsite.NestedR"; here we only define the host site,+-- the subsite's top-level 'YesodSubDispatch' instance (which delegates to the+-- separately compiled 'YesodSubDispatchNested' instance), and the leaf+-- handler. We never import the nested handlers — proving the split.+module YesodCoreTest.SplitSubsite.Runtime (splitSubsiteSpec) where++import Test.Hspec+import Yesod.Core+import Network.Wai (defaultRequest, pathInfo)+import Network.Wai.Test+import Data.Text (Text)+import qualified Data.ByteString.Lazy.Char8 as L8++import YesodCoreTest.SplitSubsite.Data+-- Bring the split-out YesodSubDispatchNested instance into scope. We import+-- the instance only — none of the nested handlers leak into this module.+import YesodCoreTest.SplitSubsite.NestedR ()++data App = App { getSplit :: SplitSub }++mkYesod "App" [parseRoutes|+/split SplitSubR SplitSub getSplit+|]++instance Yesod App++getSplitHomeR :: SubHandlerFor SplitSub master Text+getSplitHomeR = pure "splitHome"++instance YesodSubDispatch SplitSub master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesSplitSub)++app :: App+app = App { getSplit = SplitSub }++runner :: Session () -> IO ()+runner f = toWaiApp app >>= runSession f++splitSubsiteSpec :: Spec+splitSubsiteSpec = describe "YesodCoreTest.SplitSubsite.Runtime (split subsite routes)" $ do+ it "dispatches the subsite's own leaf route" $ runner $ do+ res <- request defaultRequest { pathInfo = ["split"] }+ assertStatus 200 res+ assertBody (L8.pack "splitHome") res+ it "delegates the nested home route to the split-out module" $ runner $ do+ res <- request defaultRequest { pathInfo = ["split", "nested"] }+ assertStatus 200 res+ assertBody (L8.pack "nestedHome") res+ it "delegates a nested dynamic route to the split-out module" $ runner $ do+ res <- request defaultRequest { pathInfo = ["split", "nested", "detail", "7"] }+ assertStatus 200 res+ assertBody (L8.pack "nestedDetail:7") res
test/YesodCoreTest/Ssl.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.Ssl ( sslOnlySpec, unsecSpec, sameSiteSpec ) where import qualified YesodCoreTest.StubSslOnly as Ssl import qualified YesodCoreTest.StubLaxSameSite as LaxSameSite@@ -43,7 +48,7 @@ cookiesIn r = DL.map (Cookie.parseSetCookie . snd)- (DL.filter (("Set-Cookie"==) . fst) $ simpleHeaders r)+ (DL.filter (("Set-Cookie" ==) . fst) $ simpleHeaders r) sslOnlySpec :: Spec sslOnlySpec = describe "A Yesod application with sslOnly on" $ do
test/YesodCoreTest/Streaming.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+ module YesodCoreTest.Streaming (specs) where import Yesod.Core
test/YesodCoreTest/StubLaxSameSite.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.StubLaxSameSite ( App ( App ) , Widget
test/YesodCoreTest/StubSslOnly.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.StubSslOnly ( App ( App ) , Widget
test/YesodCoreTest/StubStrictSameSite.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.StubStrictSameSite ( App ( App ) , Widget
test/YesodCoreTest/StubUnsecured.hs view
@@ -1,4 +1,9 @@-{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+ module YesodCoreTest.StubUnsecured ( App ( App ) , Widget
+ test/YesodCoreTest/SubSub.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module YesodCoreTest.SubSub where++import Test.Hspec++import Yesod.Core+import Network.Wai.Test+import qualified Data.Text as T+import qualified Data.ByteString.Lazy.Char8 as L8++import YesodCoreTest.SubSubData++data App = App { getOuter :: OuterSubSite }++mkYesod "App" [parseRoutes|+/ OuterSubSiteR OuterSubSite getOuter+|]++instance Yesod App++getSubR :: SubHandlerFor InnerSubSite master T.Text+getSubR = return $ T.pack "sub"++instance YesodSubDispatch OuterSubSite master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesOuterSubSite)++instance YesodSubDispatch InnerSubSite master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesInnerSubSite)++app :: App+app = App { getOuter = OuterSubSite { getInner = InnerSubSite }}++runner :: Session () -> IO ()+runner f = toWaiApp app >>= runSession f++case_subSubsite :: IO ()+case_subSubsite = runner $ do+ res <- request defaultRequest+ assertBody (L8.pack "sub") res+ assertStatus 200 res++subSubTest :: Spec+subSubTest = describe "YesodCoreTest.SubSub" $ do+ it "sub_subsite" case_subSubsite
+ test/YesodCoreTest/SubSubData.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module YesodCoreTest.SubSubData where++import Yesod.Core+++data OuterSubSite = OuterSubSite { getInner :: InnerSubSite }++data InnerSubSite = InnerSubSite++mkYesodSubData "InnerSubSite" [parseRoutes|+/ SubR GET+|]++mkYesodSubData "OuterSubSite" [parseRoutes|+/ InnerSubSiteR InnerSubSite getInner+|]
+ test/YesodCoreTest/SubsiteFallthrough/Data.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Subsite route data for the nested-route fallthrough runtime test. Both+-- subsites have a nested parent @WrapR@ containing two sibling sub-parents that+-- share the @\/foo@ prefix: the first (@FooAR@) matches only an empty suffix+-- (its index), the second (@FooBR@) matches @\/bar@. A request for+-- @\/wrap\/foo\/bar@ therefore misses in @FooAR@ and can only reach @FooBR@ by+-- falling through — the behaviour the dispatch modules toggle.+--+-- The route data lives here, apart from the 'mkNestedSubDispatchInstance'+-- splices in "YesodCoreTest.SubsiteFallthrough.Nested", because the+-- generated @resources*@ binding can't feed a splice in the same module (GHC+-- stage restriction) — the same split "YesodCoreTest.SplitSubsite.Runtime" uses.+module YesodCoreTest.SubsiteFallthrough.Data where++import Yesod.Core++data FallOnSub = FallOnSub++mkYesodSubData "FallOnSub" [parseRoutes|+/wrap OnWrapR:+ /foo OnFooAR:+ / OnFooAIndexR GET+ /foo OnFooBR:+ /bar OnFooBBarR GET+|]++data FallOffSub = FallOffSub++mkYesodSubData "FallOffSub" [parseRoutes|+/wrap OffWrapR:+ /foo OffFooAR:+ / OffFooAIndexR GET+ /foo OffFooBR:+ /bar OffFooBBarR GET+|]
+ test/YesodCoreTest/SubsiteFallthrough/Nested.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | The split-out dispatch module for the fallthrough subsites' nested+-- @WrapR@ fragments. The nested handlers live here, alongside the+-- 'YesodSubDispatchNested' instances generated by 'mkNestedSubDispatchInstance'+-- — @FallOnSub@ with fallthrough enabled, @FallOffSub@ at the default.+--+-- Compiled separately from "YesodCoreTest.SubsiteFallthrough.Runtime" so the host+-- site's 'mkYesodSubDispatch' splice sees these instances via 'isInstance' and+-- delegates @WrapR@ to them; the delegated 'YesodSubDispatchNested' is what+-- routes through 'genNestedDispatchClauses' and so honours the fallthrough+-- flag baked in here.+module YesodCoreTest.SubsiteFallthrough.Nested () where++import Yesod.Core+import Yesod.Core.Dispatch+ (mkNestedSubDispatchInstance, TyArgs(..))+import Data.Text (Text)++import YesodCoreTest.SubsiteFallthrough.Data++-- Fallthrough ENABLED subsite ----------------------------------------------++getOnFooAIndexR :: SubHandlerFor FallOnSub master Text+getOnFooAIndexR = pure "onFooAIndex"++getOnFooBBarR :: SubHandlerFor FallOnSub master Text+getOnFooBBarR = pure "onFooBBar"++$(mkNestedSubDispatchInstance+ (setNestedRouteFallthrough True defaultOpts)+ "OnWrapR"+ []+ NoTyArgs+ return+ resourcesFallOnSub)++-- Fallthrough DISABLED subsite (default opts) ------------------------------++getOffFooAIndexR :: SubHandlerFor FallOffSub master Text+getOffFooAIndexR = pure "offFooAIndex"++getOffFooBBarR :: SubHandlerFor FallOffSub master Text+getOffFooBBarR = pure "offFooBBar"++$(mkNestedSubDispatchInstance+ defaultOpts+ "OffWrapR"+ []+ NoTyArgs+ return+ resourcesFallOffSub)
+ test/YesodCoreTest/SubsiteFallthrough/Runtime.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime (WAI) coverage for subsite nested-route fallthrough. Every other+-- 'setNestedRouteFallthrough' fixture is a top-level site; this mounts two+-- subsites — one with fallthrough enabled, one without — and proves that a+-- request which misses inside a nested parent reaches the sibling route only+-- when the subsite was generated with fallthrough on. See+-- "YesodCoreTest.SubsiteFallthrough.Data" for the subsite definitions.+module YesodCoreTest.SubsiteFallthrough.Runtime (specs) where++import Test.Hspec+import Yesod.Core++import YesodCoreTest.SubsiteFallthrough.Data+-- Bring the split-out YesodSubDispatchNested instances into scope (instances+-- only) so the host site's mkYesodSubDispatch delegates WrapR to them.+import YesodCoreTest.SubsiteFallthrough.Nested ()+import YesodCoreTest.RuntimeHarness (assertGet)++data FallApp = FallApp++getOnSub :: FallApp -> FallOnSub+getOnSub _ = FallOnSub++getOffSub :: FallApp -> FallOffSub+getOffSub _ = FallOffSub++mkYesod "FallApp" [parseRoutes|+/on OnSubR FallOnSub getOnSub+/off OffSubR FallOffSub getOffSub+|]++instance Yesod FallApp++-- The host site delegates each subsite's nested @WrapR@ to the+-- 'YesodSubDispatchNested' instance imported from+-- "YesodCoreTest.SubsiteFallthrough.Data".+instance YesodSubDispatch FallOnSub master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesFallOnSub)++instance YesodSubDispatch FallOffSub master where+ yesodSubDispatch = $(mkYesodSubDispatch resourcesFallOffSub)++specs :: Spec+specs = describe "YesodCoreTest.SubsiteFallthrough.Runtime (subsite nested-route fallthrough)" $ do+ describe "fallthrough enabled" $ do+ it "still reaches the shadowing nested index route" $+ assertGet FallApp 200 ["on", "wrap", "foo"] (Just "onFooAIndex")+ it "falls through a nested miss to the sibling route" $+ assertGet FallApp 200 ["on", "wrap", "foo", "bar"] (Just "onFooBBar")++ describe "fallthrough disabled (default)" $ do+ it "still reaches the shadowing nested index route" $+ assertGet FallApp 200 ["off", "wrap", "foo"] (Just "offFooAIndex")+ it "404s after a nested miss instead of falling through to the sibling" $+ assertGet FallApp 404 ["off", "wrap", "foo", "bar"] Nothing
+ test/YesodCoreTest/SubsiteOptsFallthrough/Data.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | Subsite route data for the 'mkYesodSubDispatchInstanceOpts' fallthrough+-- runtime test. Unlike "YesodCoreTest.SubsiteFallthrough.Data" (whose sibling+-- parents live one level down inside @WrapR@), these subsites place two+-- same-prefix parents at the subsite's *own top level* — the exact seam that+-- the @yesodSubDispatch@ body's inline parent clauses govern. @FirstFooR@+-- matches only an empty suffix (its index); @SecondFooR@ matches @\/bar@. A+-- request for @\/foo\/bar@ misses inside @FirstFooR@ and can only reach+-- @SecondFooR@ by falling through — the behaviour that+-- 'mkYesodSubDispatchInstanceOpts' must now thread into the body via+-- 'setNestedRouteFallthrough'.+--+-- The route data lives apart from the dispatch splice (which goes in the test+-- module) because the generated @resources*@ binding can't feed a splice in+-- the same module (GHC stage restriction).+module YesodCoreTest.SubsiteOptsFallthrough.Data where++import Yesod.Core++data OptsOnSub = OptsOnSub++mkYesodSubData "OptsOnSub" [parseRoutes|+/foo OnFirstFooR:+ / OnFooIndexR GET+/foo OnSecondFooR:+ /bar OnFooBarR GET+|]++data OptsOffSub = OptsOffSub++mkYesodSubData "OptsOffSub" [parseRoutes|+/foo OffFirstFooR:+ / OffFooIndexR GET+/foo OffSecondFooR:+ /bar OffFooBarR GET+|]
+ test/YesodCoreTest/SubsiteOptsFallthrough/Runtime.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Runtime (WAI) coverage for 'mkYesodSubDispatchInstanceOpts' with+-- 'setNestedRouteFallthrough'. The headline use case of that entry point —+-- a subsite whose own top-level parent clauses fall through to a later sibling+-- on an inner miss — was previously a no-op: the generated @yesodSubDispatch@+-- body came from the opts-less 'mkYesodSubDispatch', which hardcoded+-- @mdsNestedRouteFallthrough = False@. This pins the fix.+--+-- Two subsites are mounted: 'OptsOnSub' generated with fallthrough on,+-- 'OptsOffSub' at the default. Both have two same-prefix top-level parents+-- (@\/foo FirstFooR@ then @\/foo SecondFooR@); @\/foo\/bar@ reaches the+-- @SecondFooR@ sibling only when the body honours the flag. See+-- "YesodCoreTest.SubsiteOptsFallthrough.Data" for the route data.+module YesodCoreTest.SubsiteOptsFallthrough.Runtime (specs) where++import Data.Text (Text)+import Test.Hspec+import Yesod.Core+import Yesod.Core.Dispatch (mkYesodSubDispatchInstanceOpts)++import YesodCoreTest.SubsiteOptsFallthrough.Data+import YesodCoreTest.RuntimeHarness (assertGet)++-- Handlers for the fallthrough-ENABLED subsite.+getOnFooIndexR :: SubHandlerFor OptsOnSub master Text+getOnFooIndexR = pure "onFooIndex"++getOnFooBarR :: SubHandlerFor OptsOnSub master Text+getOnFooBarR = pure "onFooBar"++-- Handlers for the fallthrough-DISABLED subsite.+getOffFooIndexR :: SubHandlerFor OptsOffSub master Text+getOffFooIndexR = pure "offFooIndex"++getOffFooBarR :: SubHandlerFor OptsOffSub master Text+getOffFooBarR = pure "offFooBar"++-- The API under test: the @yesodSubDispatch@ body must thread the fallthrough+-- flag from these opts so the subsite's own top-level @FirstFooR@/@SecondFooR@+-- siblings fall through. Placed before 'mkYesod' so the host dispatch sees the+-- generated subsite instances.+mkYesodSubDispatchInstanceOpts+ (setNestedRouteFallthrough True defaultOpts)+ "OptsOnSub"+ resourcesOptsOnSub++mkYesodSubDispatchInstanceOpts+ defaultOpts+ "OptsOffSub"+ resourcesOptsOffSub++data OptsFallApp = OptsFallApp++getOnSub :: OptsFallApp -> OptsOnSub+getOnSub _ = OptsOnSub++getOffSub :: OptsFallApp -> OptsOffSub+getOffSub _ = OptsOffSub++mkYesod "OptsFallApp" [parseRoutes|+/on OnSubR OptsOnSub getOnSub+/off OffSubR OptsOffSub getOffSub+|]++instance Yesod OptsFallApp++specs :: Spec+specs = describe "YesodCoreTest.SubsiteOptsFallthrough.Runtime (mkYesodSubDispatchInstanceOpts fallthrough)" $ do+ describe "fallthrough enabled (setNestedRouteFallthrough True)" $ do+ it "still reaches the shadowing top-level index route" $+ assertGet OptsFallApp 200 ["on", "foo"] (Just "onFooIndex")+ it "falls through a top-level parent miss to the sibling route" $+ assertGet OptsFallApp 200 ["on", "foo", "bar"] (Just "onFooBar")++ describe "fallthrough disabled (default)" $ do+ it "still reaches the shadowing top-level index route" $+ assertGet OptsFallApp 200 ["off", "foo"] (Just "offFooIndex")+ it "404s after a top-level parent miss instead of falling through" $+ assertGet OptsFallApp 404 ["off", "foo", "bar"] Nothing
test/YesodCoreTest/WaiSubsite.hs view
@@ -1,4 +1,11 @@-{-# LANGUAGE CPP, QuasiQuotes, TemplateHaskell, TypeFamilies, MultiParamTypeClasses, OverloadedStrings, ViewPatterns #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.WaiSubsite ( specs , Widget
test/YesodCoreTest/Widget.hs view
@@ -1,6 +1,11 @@-{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleInstances, ViewPatterns #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}+ module YesodCoreTest.Widget ( widgetTest , resourcesY@@ -98,7 +103,7 @@ assertBody "<!DOCTYPE html>\n<html><head><title></title><script>toHead</script><toHead></toHead>\n<style>toWidget{bar:baz}toHead{bar:baz}</style></head><body><script>toBody</script><p>toWidget</p>\n<p>toBody</p>\n<script>toWidget</script></body></html>" res runner :: Session () -> IO ()-runner f = toWaiApp Y >>= runSession f+runner f = toWaiAppPlain Y >>= runSession f case_addJuliusBody :: IO () case_addJuliusBody = runner $ do
+ test/YesodCoreTest/ZeroPieceShadow/FallApp.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | The same zero-piece-parent shape as "YesodCoreTest.ZeroPieceShadow.ShadowApp",+-- but built with @'setNestedRouteFallthrough' True@. With fallthrough enabled+-- the @WildP@ parent no longer swallows non-matching paths, so the sibling+-- @\/sibling@ becomes reachable. Pinned by "YesodCoreTest.ZeroPieceShadowRuntime".+module YesodCoreTest.ZeroPieceShadow.FallApp+ ( FallApp (..)+ ) where++import Yesod.Core++data FallApp = FallApp++mkYesodOpts (setNestedRouteFallthrough True defaultOpts) "FallApp" [parseRoutes|+/ FallParentR:+ /child FallChildR GET+/sibling FallSiblingR GET+|]++instance Yesod FallApp where+ messageLoggerSource = mempty++getFallChildR :: HandlerFor FallApp String+getFallChildR = pure "FallChildR"++getFallSiblingR :: HandlerFor FallApp String+getFallSiblingR = pure "FallSiblingR"
+ test/YesodCoreTest/ZeroPieceShadow/ShadowApp.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++-- | A zero-piece nested parent (pattern @\/@) followed by a sibling, built with+-- the default @fallthrough = False@. The parent's match pattern is @WildP@+-- (unconditional), so @\/sibling@ is shadowed: it is swallowed by the parent+-- and 404s. See "YesodCoreTest.ZeroPieceShadow.FallApp" for the fallthrough+-- counterpart. Pinned by "YesodCoreTest.ZeroPieceShadowRuntime".+module YesodCoreTest.ZeroPieceShadow.ShadowApp+ ( ShadowApp (..)+ ) where++import Yesod.Core++data ShadowApp = ShadowApp++mkYesodOpts defaultOpts "ShadowApp" [parseRoutes|+/ ShadowParentR:+ /child ShadowChildR GET+/sibling SiblingR GET+|]++instance Yesod ShadowApp where+ messageLoggerSource = mempty++getShadowChildR :: HandlerFor ShadowApp String+getShadowChildR = pure "ShadowChildR"++getSiblingR :: HandlerFor ShadowApp String+getSiblingR = pure "SiblingR"
+ test/YesodCoreTest/ZeroPieceShadowRuntime.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Regression coverage for the documented edge case: a nested parent route+-- with /no/ leading path piece (pattern @\/@) compiles to an unconditional+-- first match ('mkPathPat' @EndWild []@ = @WildP@), so any sibling route+-- declared after it is unreachable under the default+-- @'setNestedRouteFallthrough' False@. Enabling fallthrough lets a non-matching+-- subtree fall through to the siblings.+--+-- This is unchanged behaviour from previous releases (the leaf-level overlap+-- check never flagged it, since the parent contributes zero pieces and the+-- flattened leaf paths @\/child@ vs @\/sibling@ do not overlap). The+-- @EndWild@/@PathTail@ abstraction made the shadow silent, so it is pinned here.+module YesodCoreTest.ZeroPieceShadowRuntime+ ( specs+ ) where++import Test.Hspec+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text)+import YesodCoreTest.ZeroPieceShadow.ShadowApp (ShadowApp (..))+import YesodCoreTest.ZeroPieceShadow.FallApp (FallApp (..))+import YesodCoreTest.RuntimeHarness (assertGet)++shadowReq :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+shadowReq = assertGet ShadowApp++fallReq :: HasCallStack => Int -> [Text] -> Maybe ByteString -> IO ()+fallReq = assertGet FallApp++specs :: Spec+specs = describe "zero-piece nested parent shadowing siblings" $ do+ describe "default fallthrough = False (the parent's WildP shadows the sibling)" $ do+ it "still dispatches the parent's own child (/child)" $+ shadowReq 200 ["child"] (Just "ShadowChildR")+ it "404s the shadowed sibling (/sibling), which the WildP parent swallows" $+ shadowReq 404 ["sibling"] Nothing++ describe "setNestedRouteFallthrough True (the sibling becomes reachable)" $ do+ it "still dispatches the parent's own child (/child)" $+ fallReq 200 ["child"] (Just "FallChildR")+ it "falls through the WildP parent to the sibling (/sibling)" $+ fallReq 200 ["sibling"] (Just "FallSiblingR")
− test/fixtures/routes_with_line_continuations
@@ -1,11 +0,0 @@--- This fixture to test line continuations is in a separate file--- because when I put it in an in-line quasi-quotation, the compiler--- performed the line continuations processing itself.--/foo1 \- Foo1-/foo2 Foo2-/foo3 \- Foo3 \- GET POST \- !foo
+ test/fixtures/routes_with_line_continuations.yesodroutes view
@@ -0,0 +1,11 @@+-- This fixture to test line continuations is in a separate file+-- because when I put it in an in-line quasi-quotation, the compiler+-- performed the line continuations processing itself.++/foo1 \+ Foo1+/foo2 Foo2+/foo3 \+ Foo3 \+ GET POST \+ !foo
yesod-core.cabal view
@@ -1,5 +1,5 @@ name: yesod-core-version: 1.6.14+version: 1.7.0.0 license: MIT license-file: LICENSE author: Michael Snoyman <michael@snoyman.com>@@ -8,7 +8,7 @@ description: API docs and the README are available at <http://www.stackage.org/package/yesod-core> category: Web, Yesod stability: Stable-cabal-version: >= 1.8+cabal-version: >= 1.10 build-type: Simple homepage: http://www.yesodweb.com/ extra-source-files:@@ -17,15 +17,18 @@ test/YesodCoreTest/JsLoaderSites/Bottom.hs test/en.msg test/test.hs- test/fixtures/routes_with_line_continuations+ test/fixtures/routes_with_line_continuations.yesodroutes ChangeLog.md README.md+ docs/split-route-compilation.md library+ default-language: Haskell2010 hs-source-dirs: src - build-depends: base >= 4.9 && < 5+ build-depends: base >= 4.11 && < 5 , aeson >= 1.0+ , attoparsec-aeson >= 2.1 , auto-update , blaze-html >= 0.5 , blaze-markup >= 0.7.1@@ -36,8 +39,11 @@ , conduit >= 1.3 , conduit-extra , containers >= 0.2- , cookie >= 0.4.3 && < 0.5+ , cookie >= 0.4.3 && < 0.6+ , data-default , deepseq >= 1.3+ , encoding+ , entropy , fast-logger >= 2.2 , http-types >= 0.7 , memory@@ -46,18 +52,19 @@ , parsec >= 2 && < 3.2 , path-pieces >= 0.1.2 && < 0.3 , primitive >= 0.6- , random >= 1.0.0.2 && < 1.2+ , random >= 1.0.0.2 && < 1.4 , resourcet >= 1.2- , rio , shakespeare >= 2.0- , template-haskell >= 2.11+ , template-haskell >= 2.13 , text >= 0.7+ , th-compat >= 0.1+ , th-lift-instances >= 0.1 , time >= 1.5 , transformers >= 0.4 , unix-compat , unliftio , unordered-containers >= 0.2- , vector >= 0.9 && < 0.13+ , vector >= 0.9 && < 0.14 , wai >= 3.2 , wai-extra >= 3.0.7 , wai-logger >= 0.2@@ -74,6 +81,7 @@ Yesod.Core.Types Yesod.Core.Unsafe Yesod.Routes.TH.Types+ Yesod.Core.Class.Dispatch.ToParentRoute other-modules: Yesod.Core.Internal.Session Yesod.Core.Internal.Request Yesod.Core.Class.Handler@@ -86,6 +94,10 @@ Yesod.Core.Class.Dispatch Yesod.Core.Class.Breadcrumbs Yesod.Core.TypeCache+ Yesod.Core.Types.Content+ Yesod.Core.Types.ErrorResponse+ Yesod.Core.Types.HandlerContents+ Yesod.Core.Types.TypedContent Paths_yesod_core Yesod.Routes.TH@@ -96,21 +108,26 @@ Yesod.Routes.TH.RenderRoute Yesod.Routes.TH.ParseRoute Yesod.Routes.TH.RouteAttrs+ Yesod.Routes.TH.Internal ghc-options: -Wall- -- Following line added due to: https://github.com/yesodweb/yesod/issues/545- -- This looks like a GHC bug- extensions: MultiParamTypeClasses -- Workaround for: http://ghc.haskell.org/trac/ghc/ticket/8443- extensions: TemplateHaskell+ other-extensions: TemplateHaskell test-suite test-routes+ default-language: Haskell2010 type: exitcode-stdio-1.0 main-is: RouteSpec.hs hs-source-dirs: test, src other-modules: Hierarchy+ Hierarchy.Admin+ Hierarchy.ResourceTree+ Hierarchy.Nest2.NestInner+ Hierarchy.Nest2+ Hierarchy.Nest3+ Hierarchy.Nest Yesod.Routes.Class Yesod.Routes.Overlap Yesod.Routes.Parse@@ -120,34 +137,99 @@ Yesod.Routes.TH.RenderRoute Yesod.Routes.TH.RouteAttrs Yesod.Routes.TH.Types+ Yesod.Routes.TH.Internal+ Route.FallthroughSpec+ YesodCoreTest.RuntimeHarness+ Route.RenderRouteSpec+ Route.RouteAttrSpec+ Route.DiscoveryModeSpec+ Route.SubDispatchAritySpec+ Route.DeepAritySpec+ Route.DeepArityTypes+ Route.InstanceProbeSpec+ Route.InstanceProbeTypes+ Route.InlineParseClausesSpec+ Route.NestedParseClausesSpec+ Route.PureQ+ Route.FocusLeafConsSpec+ Route.MissingFocusTargetSpec+ Route.MissingFocusTargetTypes+ YesodCoreTest.RenderRouteSpec+ YesodCoreTest.RenderRouteSpec.TH -- Workaround for: http://ghc.haskell.org/trac/ghc/ticket/8443- extensions: TemplateHaskell+ other-extensions: TemplateHaskell - build-depends: base- , hspec- , containers+ build-depends: base < 5+ , HUnit+ , aeson+ , attoparsec-aeson+ , auto-update+ , blaze-html+ , blaze-markup , bytestring+ , hspec-expectations+ , case-insensitive+ , cereal+ , clientsession+ , conduit+ , conduit-extra+ , containers+ , cookie+ , data-default+ , deepseq+ , encoding+ , entropy+ , fast-logger+ , hspec+ , http-types+ , memory+ , monad-logger+ , mtl+ , parsec+ , path-pieces+ , primitive+ , random+ , resourcet+ , shakespeare , template-haskell , text- , random- , path-pieces- , HUnit+ , th-abstraction+ , th-compat+ , th-lift-instances+ , time+ , transformers+ , unix-compat+ , unliftio+ , unordered-containers+ , vector+ , wai+ , wai-extra+ , wai-logger+ , warp+ , word8 test-suite tests+ default-language: Haskell2010 type: exitcode-stdio-1.0 main-is: test.hs hs-source-dirs: test other-modules: YesodCoreTest YesodCoreTest.Auth+ YesodCoreTest.Breadcrumb YesodCoreTest.Cache YesodCoreTest.CleanPath+ YesodCoreTest.Content YesodCoreTest.Header YesodCoreTest.Csrf YesodCoreTest.ErrorHandling+ YesodCoreTest.ErrorHandling.CustomApp YesodCoreTest.Exceptions+ YesodCoreTest.FallthroughDispatch.Runtime+ YesodCoreTest.FallthroughDispatch.Resources YesodCoreTest.InternalRequest+ YesodCoreTest.JsAttributes YesodCoreTest.JsLoader YesodCoreTest.JsLoaderSites.Bottom YesodCoreTest.Json@@ -155,8 +237,21 @@ YesodCoreTest.LiteApp YesodCoreTest.Media YesodCoreTest.MediaData+ YesodCoreTest.Meta+ YesodCoreTest.NestedDispatch.Runtime+ YesodCoreTest.NestedDispatch.NestR+ YesodCoreTest.NestedDispatch.InnerR+ YesodCoreTest.NestedDispatch.ParentR+ YesodCoreTest.NestedDispatch.Parent0R+ YesodCoreTest.NestedDispatch.Parent0R.Child0R+ YesodCoreTest.NestedDispatch.Resources YesodCoreTest.NoOverloadedStrings YesodCoreTest.NoOverloadedStringsSub+ YesodCoreTest.ParameterizedSite+ YesodCoreTest.ParameterizedSite.Compat+ YesodCoreTest.ParameterizedSite.PolyAny+ YesodCoreTest.ParameterizedSite.PolyShow+ YesodCoreTest.ParameterizedSite.SubRoute YesodCoreTest.RawResponse YesodCoreTest.Redirect YesodCoreTest.Reps@@ -167,6 +262,44 @@ YesodCoreTest.StubSslOnly YesodCoreTest.StubStrictSameSite YesodCoreTest.StubUnsecured+ YesodCoreTest.SubSub+ YesodCoreTest.SubSubData+ YesodCoreTest.SplitSubsite.Runtime+ YesodCoreTest.SplitSubsite.Data+ YesodCoreTest.SplitSubsite.NestedR+ YesodCoreTest.ParameterizedSubData+ YesodCoreTest.ParamSubsite.Data+ YesodCoreTest.ParameterizedSubDispatch+ YesodCoreTest.ParameterizedSubDispatch.Data+ YesodCoreTest.ParameterizedSubDispatchRuntime+ YesodCoreTest.ParameterizedSubDispatchRuntime.Data+ YesodCoreTest.ParamSubsite.InstanceRuntime+ YesodCoreTest.RuntimeHarness+ YesodCoreTest.ParamSubsite.SplitNested+ YesodCoreTest.ParamSubsite.SplitRuntime+ YesodCoreTest.ParamDefaultSplit.Data+ YesodCoreTest.ParamDefaultSplit.Runtime+ YesodCoreTest.ParamTopLevelRuntime+ YesodCoreTest.ParamFocusSplit.Runtime+ YesodCoreTest.ParamFocusSplit.Resources+ YesodCoreTest.ParamFocusSplit.SubR+ YesodCoreTest.ParamNoExplicitArgs+ YesodCoreTest.ParamFallthroughRuntime+ YesodCoreTest.ParamNoFallthroughRuntime+ YesodCoreTest.ParamNestedNoFallthroughRuntime+ YesodCoreTest.FallthroughMatrix.Resources+ YesodCoreTest.FallthroughMatrix.FirstFoo+ YesodCoreTest.FallthroughMatrix.Runtime+ YesodCoreTest.MultiPieceNestedRuntime+ YesodCoreTest.ZeroPieceShadowRuntime+ YesodCoreTest.ZeroPieceShadow.ShadowApp+ YesodCoreTest.ZeroPieceShadow.FallApp+ YesodCoreTest.BangSeparatorRuntime+ YesodCoreTest.SubsiteFallthrough.Runtime+ YesodCoreTest.SubsiteFallthrough.Data+ YesodCoreTest.SubsiteFallthrough.Nested+ YesodCoreTest.SubsiteOptsFallthrough.Runtime+ YesodCoreTest.SubsiteOptsFallthrough.Data YesodCoreTest.WaiSubsite YesodCoreTest.Widget YesodCoreTest.YesodTest@@ -174,14 +307,16 @@ cpp-options: -DTEST if os(windows) cpp-options: -DWINDOWS- build-depends: base+ build-depends: base >= 4.11 && < 5 , async , bytestring , clientsession , conduit , conduit-extra , containers- , cookie >= 0.4.1 && < 0.5+ , cookie >= 0.4.1 && < 0.6+ , data-default+ , encoding , hspec >= 1.3 , hspec-expectations , http-types@@ -190,20 +325,52 @@ , resourcet , shakespeare , streaming-commons+ , template-haskell , text , transformers , unliftio+ , unordered-containers+ , path-pieces+ , mtl+ , time+ , cereal+ , monad-logger+ , deepseq+ , blaze-html+ , aeson+ , primitive+ , case-insensitive+ , wai-logger+ , memory+ , unix-compat+ , auto-update+ , vector+ , word8+ , blaze-markup+ , fast-logger+ , parsec , wai >= 3.0+ , entropy+ , attoparsec-aeson , wai-extra , warp+ , th-lift-instances , yesod-core+ -- NB: this behavioral suite depends on the *packaged* yesod-core library+ -- (no `src` on hs-source-dirs), so it compiles against the real+ -- exposed/hidden module boundary. That is what catches API-exposure+ -- regressions (a module that downstream consumers cannot import). The+ -- pure-codegen suite `test-routes` keeps `src` on hs-source-dirs because it+ -- legitimately needs internal modules (e.g. Yesod.Routes.TH.RenderRoute,+ -- Yesod.Routes.Class) for compile-time codegen assertions. ghc-options: -Wall -threaded- extensions: TemplateHaskell+ other-extensions: TemplateHaskell benchmark widgets+ default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: bench- build-depends: base+ build-depends: base < 5 , blaze-html , bytestring , gauge