@@ -77,9 +77,11 @@ defmodule Phoenix.VerifiedRoutes do
7777 To verify routes in your application modules, such as controller, templates, and views,
7878 `use Phoenix.VerifiedRoutes`, which supports the following options:
7979
80- * `:router` - The required router to verify ~p paths against
81- * `:endpoint` - The optional endpoint for ~p script_name and URL generation
82- * `:statics` - The optional list of static directories to treat as verified paths
80+ * `:router` - The required router to verify `~p` paths against
81+ * `:endpoint` - Optional endpoint for URL generation
82+ * `:statics` - Optional list of static directories to treat as verified paths
83+ * `:path_prefixes` - Optional list of path prefixes to be added to every generated path.
84+ See "Path prefixes" for more information
8385
8486 For example:
8587
@@ -88,7 +90,7 @@ defmodule Phoenix.VerifiedRoutes do
8890 endpoint: AppWeb.Endpoint,
8991 statics: ~w(images)
9092
91- ## Usage
93+ ## Connection/socket-based route generation
9294
9395 The majority of path and URL generation needs your application will be met
9496 with `~p` and `url/1`, where all information necessary to construct the path
@@ -108,32 +110,77 @@ defmodule Phoenix.VerifiedRoutes do
108110 such as library code, or application code that relies on multiple routers. In such cases,
109111 the router module can be provided explicitly to `path/3` and `url/3`.
110112
111- ## Tracking Warnings
113+ ## Tracking warnings
112114
113115 All static path segments must start with forward slash, and you must have a static segment
114116 between dynamic interpolations in order for a route to be verified without warnings.
115- For example, the following path generates proper warnings
117+ For example, imagine you have these two routes:
116118
117- ~p"/media/posts/#{post}"
119+ get "/media/posts/:id"
120+ get "/media/images/:id"
118121
119- While this one will not allow the compiler to see the full path :
122+ The following route will be verified and emit a warning as it does not match the router :
120123
121- type = "posts"
124+ ~p"/media/post/#{post}"
125+
126+ However the one below will not, the "post" segment is dynamic:
127+
128+ type = "post"
122129 ~p"/media/#{type}/#{post}"
123130
124- In such cases, it's better to write a function such as `media_path/1` which branches
125- on different `~p`'s to handle each type.
131+ If you find yourself needing to generate dynamic URLs which are defined statically
132+ in the router, that's a good indicator you should refactor it into one or more
133+ function, such as `posts_path/1` and `images_path/1`.
126134
127135 Like any other compilation warning, the Elixir compiler will warn any time the file
128- that a ~p resides in changes, or if the router is changed. To view previously issued
129- warnings for files that lack new changes, the `--all-warnings` flag may be passed to
130- the `mix compile` task. For the following will show all warnings the compiler
131- has previously encountered when compiling the current application code:
136+ that a `~p` resides in changes, or if the router is changed.
137+
138+ ## Localized routes and path prefixes
139+
140+ Applications that need to support internationalization (i18n) and localization (l10n)
141+ often do so at the URL level. In such cases, there are different approaches one can
142+ choose.
143+
144+ One option is to perform i18n at the domain level. You can have `example.com` (in which
145+ you would detect the locale based on the "Accept-Language" HTTP header), `en.example.com`,
146+ `en-GB.example.com` and so forth. In this case, you would have a plug that looks at the
147+ host and at HTTP headers and calls `Gettext.get_locale/1` accordingly. The biggest benefit
148+ of this approach is that you don't have to change the routes in your application and
149+ verified routes works as is.
150+
151+ Some applications, however, like to the locale as part of the URL prefix:
152+
153+ scope "/:locale" do
154+ get "/posts"
155+ get "/images"
156+ end
132157
133- $ mix compile --all-warnings
158+ For such cases, VerifiedRoutes allow you to configure a `path_prefixes` option, which
159+ is a list of segments to prepend to the URL. For example:
134160
135- *Note: Elixir >= 1.14.0 is required for comprehensive warnings. Older versions
136- will compile properly, but no warnings will be issued.
161+ use Phoenix.VerifiedRoutes,
162+ router: AppWeb.Router,
163+ endpoint: AppWeb.Endpoint,
164+ path_prefixes: [{Gettext, :get_locale, []}]
165+
166+ The above will prepend `"/#{Gettext.get_locale()}"` to every path and url generated with
167+ `~p`. If your website has a handful of URLs that do not require the locale prefix, then
168+ we suggest defining them in a separate module, where you use `Phoenix.VerifiedRoutes`
169+ without the prefix option:
170+
171+ defmodule UnlocalizedRoutes do
172+ use Phoenix.VerifiedRoutes,
173+ router: AppWeb.Router,
174+ endpoint: AppWeb.Endpoint,
175+
176+ # Since :path_prefixes was not declared,
177+ # the code below won't prepend the locale and still be verified
178+ def root, do: ~p"/"
179+ end
180+
181+ Finally, for even more complex use cases, where the whole URL needs to localized,
182+ see projects such as [Routex](https://github.com/BartOtten/routex) and
183+ [CLDR.Routes](https://github.com/elixir-cldr/cldr_routes).
137184 '''
138185 @ doc false
139186 defstruct router: nil ,
@@ -175,7 +222,20 @@ defmodule Phoenix.VerifiedRoutes do
175222 other -> raise ArgumentError , "expected statics to be a list, got: #{ inspect ( other ) } "
176223 end
177224
178- Module . put_attribute ( mod , :phoenix_verified_statics , statics )
225+ path_prefixes =
226+ case Keyword . get ( opts , :path_prefixes , [ ] ) do
227+ list when is_list ( list ) ->
228+ list
229+
230+ other ->
231+ raise ArgumentError ,
232+ "expected path_prefixes to be a list of zero-arity functions, got: #{ inspect ( other ) } "
233+ end
234+
235+ Module . put_attribute ( mod , :phoenix_verified_config , % {
236+ statics: statics ,
237+ path_prefixes: path_prefixes
238+ } )
179239 end
180240
181241 @ after_verify_supported Version . match? ( System . version ( ) , ">= 1.14.0" )
@@ -805,7 +865,7 @@ defmodule Phoenix.VerifiedRoutes do
805865 end
806866
807867 defp build_route ( route_ast , sigil_p , env , endpoint_ctx , router ) do
808- statics = Module . get_attribute ( env . module , :phoenix_verified_statics , [ ] )
868+ config = Module . get_attribute ( env . module , :phoenix_verified_config , [ ] )
809869
810870 router =
811871 case Macro . expand ( router , env ) do
@@ -821,7 +881,7 @@ defmodule Phoenix.VerifiedRoutes do
821881 end
822882
823883 { static? , meta , test_path , path_ast , static_ast } =
824- rewrite_path ( route_ast , endpoint_ctx , router , statics )
884+ rewrite_path ( route_ast , endpoint_ctx , router , config )
825885
826886 route = % __MODULE__ {
827887 router: router ,
@@ -844,25 +904,30 @@ defmodule Phoenix.VerifiedRoutes do
844904 end
845905 end
846906
847- defp rewrite_path ( route , endpoint , router , statics ) do
907+ defp rewrite_path ( route , endpoint , router , config ) do
848908 { :<<>> , meta , segments } = route
849909 { path_rewrite , query_rewrite } = verify_segment ( segments , route )
910+ path_rewrite = compile_prefixes ( config . path_prefixes , meta ) ++ path_rewrite
850911
851912 rewrite_route =
852- quote generated: true do
853- query_str = unquote ( { :<<>> , meta , query_rewrite } )
854- path_str = unquote ( { :<<>> , meta , path_rewrite } )
913+ if query_rewrite == [ ] do
914+ { :<<>> , meta , path_rewrite }
915+ else
916+ quote generated: true do
917+ query_str = unquote ( { :<<>> , meta , query_rewrite } )
918+ path_str = unquote ( { :<<>> , meta , path_rewrite } )
855919
856- if query_str == "" do
857- path_str
858- else
859- path_str <> "?" <> query_str
920+ if query_str == "" do
921+ path_str
922+ else
923+ path_str <> "?" <> query_str
924+ end
860925 end
861926 end
862927
863928 test_path = Enum . map_join ( path_rewrite , & if ( is_binary ( & 1 ) , do: & 1 , else: "1" ) )
864929
865- static? = static_path? ( test_path , statics )
930+ static? = static_path? ( test_path , config . statics )
866931
867932 path_ast =
868933 quote generated: true do
@@ -877,6 +942,21 @@ defmodule Phoenix.VerifiedRoutes do
877942 { static? , meta , test_path , path_ast , static_ast }
878943 end
879944
945+ defp compile_prefixes ( path_prefixes , meta ) do
946+ Enum . flat_map ( path_prefixes , fn
947+ { module , fun , args } when is_atom ( module ) and is_atom ( fun ) and is_list ( args ) ->
948+ [
949+ "/" ,
950+ { :"::" , meta ,
951+ [ { { :. , meta , [ module , fun ] } , meta , Macro . escape ( args ) } , { :binary , meta , nil } ] }
952+ ]
953+
954+ other ->
955+ raise ArgumentError ,
956+ ":path_prefixes option in VerifiedRoutes must be a {mod, fun, args} and return a string, got: #{ inspect ( other ) } "
957+ end )
958+ end
959+
880960 defp attr! ( % { function: nil } , _ ) do
881961 raise "Phoenix.VerifiedRoutes can only be used inside functions, please move your usage of ~p to functions"
882962 end
0 commit comments