diff --git a/waspc/data/Generator/templates/sdk/wasp/client/router/index.ts b/waspc/data/Generator/templates/sdk/wasp/client/router/index.ts index 91e2ccb95..972d03fad 100644 --- a/waspc/data/Generator/templates/sdk/wasp/client/router/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/client/router/index.ts @@ -4,6 +4,7 @@ import type { RouteDefinitionsToRoutes, OptionalRouteOptions, ParamValue, + ExpandRouteOnOptionalStaticSegments, } from './types' // PUBLIC API @@ -11,18 +12,14 @@ export const routes = { {=# routes =} {= name =}: { to: "{= urlPath =}", - {=# hasUrlParams =} build: ( - options: { + options{=^ hasUrlParams =}?{=/ hasUrlParams =}: + OptionalRouteOptions + {=# hasUrlParams =}& { params: {{=# urlParams =}{= name =}{=# isOptional =}?{=/ isOptional =}: ParamValue;{=/ urlParams =}} - } & OptionalRouteOptions, - ) => interpolatePath("{= urlPath =}", options.params, options?.search, options?.hash), - {=/ hasUrlParams =} - {=^ hasUrlParams =} - build: ( - options?: OptionalRouteOptions, - ) => interpolatePath("{= urlPath =}", undefined, options?.search, options?.hash), - {=/ hasUrlParams =} + }{=/ hasUrlParams =} + {=# hasOptionalStaticSegments =}& { path: ExpandRouteOnOptionalStaticSegments<"{= urlPath =}"> }{=/ hasOptionalStaticSegments =} + ) => interpolatePath({=# hasOptionalStaticSegments =}options.path{=/ hasOptionalStaticSegments =}{=^ hasOptionalStaticSegments =}"{= urlPath =}"{=/ hasOptionalStaticSegments =}, {=^ hasUrlParams =}undefined{=/ hasUrlParams =}{=# hasUrlParams =}options.params{=/ hasUrlParams =}, options?.search, options?.hash), }, {=/ routes =} } as const; diff --git a/waspc/data/Generator/templates/sdk/wasp/client/router/types.ts b/waspc/data/Generator/templates/sdk/wasp/client/router/types.ts index 283e21d83..784d240eb 100644 --- a/waspc/data/Generator/templates/sdk/wasp/client/router/types.ts +++ b/waspc/data/Generator/templates/sdk/wasp/client/router/types.ts @@ -2,7 +2,7 @@ export type RouteDefinitionsToRoutes = RouteDefinitionsToRoutesObj[keyof RouteDefinitionsToRoutesObj] - // PRIVATE API +// PRIVATE API export type OptionalRouteOptions = { search?: Search hash?: string @@ -40,6 +40,7 @@ type ParamsFromBuildFn = Parameters[0] extends { ? { params: Params } : { params?: never } +// PRIVATE API (sdk) /** * Optional static segments handling: expands routes with optional segments * into multiple routes, one for each possible combination of optional segments. @@ -48,7 +49,7 @@ type ParamsFromBuildFn = Parameters[0] extends { * - /users/:id * - /users/tasks/:id */ -type ExpandRouteOnOptionalStaticSegments = S extends '/' +export type ExpandRouteOnOptionalStaticSegments = S extends '/' ? '/' : `/${JoinPath>>>}` diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Client/RouterGenerator.hs b/waspc/src/Wasp/Generator/SdkGenerator/Client/RouterGenerator.hs index e84384fd9..a6fa5c283 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Client/RouterGenerator.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Client/RouterGenerator.hs @@ -12,7 +12,7 @@ import qualified Wasp.AppSpec.Route as AS.Route import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.SdkGenerator.Common as C -import Wasp.Util.WebRouterPath (Param (Optional, Required), extractPathParams) +import qualified Wasp.Util.WebRouterPath as WebRouterPath genNewClientRouterApi :: AppSpec -> Generator [FileDraft] genNewClientRouterApi spec = @@ -37,13 +37,16 @@ createRouteTemplateData (name, route) = [ "name" .= name, "urlPath" .= path, "urlParams" .= map mapPathParamToJson urlParams, - "hasUrlParams" .= (not . null $ urlParams) + "hasUrlParams" .= (not . null $ urlParams), + "hasOptionalStaticSegments" .= (not . null $ optionalStaticSegments) ] where path = AS.Route.path route - urlParams = extractPathParams path + routeSegments = WebRouterPath.getRouteSegments path + urlParams = [param | WebRouterPath.ParamSegment param <- routeSegments] + optionalStaticSegments = [segment | (WebRouterPath.StaticSegment (WebRouterPath.OptionalStaticSegment segment)) <- routeSegments] - mapPathParamToJson :: Param -> Aeson.Value - mapPathParamToJson (Required paramName) = object ["name" .= paramName, "isOptional" .= False] - mapPathParamToJson (Optional paramName) = object ["name" .= paramName, "isOptional" .= True] + mapPathParamToJson :: WebRouterPath.ParamSegment -> Aeson.Value + mapPathParamToJson (WebRouterPath.RequiredParamSegment paramName) = object ["name" .= paramName, "isOptional" .= False] + mapPathParamToJson (WebRouterPath.OptionalParamSegment paramName) = object ["name" .= paramName, "isOptional" .= True] diff --git a/waspc/src/Wasp/Util/WebRouterPath.hs b/waspc/src/Wasp/Util/WebRouterPath.hs index 5f2294354..59c190042 100644 --- a/waspc/src/Wasp/Util/WebRouterPath.hs +++ b/waspc/src/Wasp/Util/WebRouterPath.hs @@ -1,22 +1,39 @@ -module Wasp.Util.WebRouterPath where +module Wasp.Util.WebRouterPath + ( Segment (StaticSegment, ParamSegment), + StaticSegment (RequiredStaticSegment, OptionalStaticSegment), + ParamSegment (RequiredParamSegment, OptionalParamSegment), + getRouteSegments, + ) +where import Data.List (isSuffixOf) import Data.List.Split (splitOn) -import Data.Maybe (mapMaybe) -data Param = Optional String | Required String deriving (Show, Eq) +data Segment = StaticSegment StaticSegment | ParamSegment ParamSegment deriving (Show, Eq) --- TODO: upgrade to work with React Router v6: https://reactrouter.com/en/main/route/route#splats --- Maybe explode all optional segments and then compute the routes for the Link component --- This would mean we have two different lists: routes and links? -extractPathParams :: String -> [Param] -extractPathParams = mapMaybe parseParam . splitOn "/" +data StaticSegment = RequiredStaticSegment StaticSegmentValue | OptionalStaticSegment StaticSegmentValue deriving (Show, Eq) + +data ParamSegment = RequiredParamSegment ParamName | OptionalParamSegment ParamName deriving (Show, Eq) + +type StaticSegmentValue = String + +type ParamName = String + +getRouteSegments :: String -> [Segment] +getRouteSegments = map parseSegment . splitOn "/" where - parseParam :: String -> Maybe Param - parseParam "*" = Just $ Required "splat" - parseParam (':' : xs) = - Just $ - if "?" `isSuffixOf` xs - then Optional (take (length xs - 1) xs) - else Required xs - parseParam _ = Nothing + parseSegment :: String -> Segment + parseSegment "*" = ParamSegment $ RequiredParamSegment "splat" + parseSegment (':' : xs) = + ParamSegment $ + if isSegmentOptional xs + then OptionalParamSegment (take (length xs - 1) xs) + else RequiredParamSegment xs + parseSegment x = + StaticSegment $ + if isSegmentOptional x + then OptionalStaticSegment x + else RequiredStaticSegment x + +isSegmentOptional :: String -> Bool +isSegmentOptional = isSuffixOf "?"