@@ -77,9 +77,11 @@ defmodule Phoenix.VerifiedRoutes do
77
77
To verify routes in your application modules, such as controller, templates, and views,
78
78
`use Phoenix.VerifiedRoutes`, which supports the following options:
79
79
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
83
85
84
86
For example:
85
87
@@ -88,7 +90,7 @@ defmodule Phoenix.VerifiedRoutes do
88
90
endpoint: AppWeb.Endpoint,
89
91
statics: ~w(images)
90
92
91
- ## Usage
93
+ ## Connection/socket-based route generation
92
94
93
95
The majority of path and URL generation needs your application will be met
94
96
with `~p` and `url/1`, where all information necessary to construct the path
@@ -108,32 +110,77 @@ defmodule Phoenix.VerifiedRoutes do
108
110
such as library code, or application code that relies on multiple routers. In such cases,
109
111
the router module can be provided explicitly to `path/3` and `url/3`.
110
112
111
- ## Tracking Warnings
113
+ ## Tracking warnings
112
114
113
115
All static path segments must start with forward slash, and you must have a static segment
114
116
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:
116
118
117
- ~p"/media/posts/#{post}"
119
+ get "/media/posts/:id"
120
+ get "/media/images/:id"
118
121
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 :
120
123
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"
122
129
~p"/media/#{type}/#{post}"
123
130
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`.
126
134
127
135
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 add the locale as part of the URL prefix:
152
+
153
+ scope "/:locale" do
154
+ get "/posts"
155
+ get "/images"
156
+ end
132
157
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:
134
160
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://hex.pm/packages/routex) and
183
+ [`ex_cldr_routes`](https://hex.pm/packages/cldr_routes).
137
184
'''
138
185
@ doc false
139
186
defstruct router: nil ,
@@ -175,7 +222,20 @@ defmodule Phoenix.VerifiedRoutes do
175
222
other -> raise ArgumentError , "expected statics to be a list, got: #{ inspect ( other ) } "
176
223
end
177
224
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
+ } )
179
239
end
180
240
181
241
@ after_verify_supported Version . match? ( System . version ( ) , ">= 1.14.0" )
@@ -805,7 +865,7 @@ defmodule Phoenix.VerifiedRoutes do
805
865
end
806
866
807
867
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 , [ ] )
809
869
810
870
router =
811
871
case Macro . expand ( router , env ) do
@@ -821,7 +881,7 @@ defmodule Phoenix.VerifiedRoutes do
821
881
end
822
882
823
883
{ 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 )
825
885
826
886
route = % __MODULE__ {
827
887
router: router ,
@@ -844,25 +904,30 @@ defmodule Phoenix.VerifiedRoutes do
844
904
end
845
905
end
846
906
847
- defp rewrite_path ( route , endpoint , router , statics ) do
907
+ defp rewrite_path ( route , endpoint , router , config ) do
848
908
{ :<<>> , meta , segments } = route
849
909
{ path_rewrite , query_rewrite } = verify_segment ( segments , route )
910
+ path_rewrite = compile_prefixes ( config . path_prefixes , meta ) ++ path_rewrite
850
911
851
912
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 } )
855
919
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
860
925
end
861
926
end
862
927
863
928
test_path = Enum . map_join ( path_rewrite , & if ( is_binary ( & 1 ) , do: & 1 , else: "1" ) )
864
929
865
- static? = static_path? ( test_path , statics )
930
+ static? = static_path? ( test_path , config . statics )
866
931
867
932
path_ast =
868
933
quote generated: true do
@@ -877,6 +942,21 @@ defmodule Phoenix.VerifiedRoutes do
877
942
{ static? , meta , test_path , path_ast , static_ast }
878
943
end
879
944
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
+
880
960
defp attr! ( % { function: nil } , _ ) do
881
961
raise "Phoenix.VerifiedRoutes can only be used inside functions, please move your usage of ~p to functions"
882
962
end
0 commit comments