I originally had a route defined like this :
get ':foo(/:bar)/:baz(/:qux)/:slug', to: 'application#show', constraints: { baz: /. ?\-\d / }, as: test
With this configuration, it works fine if I omit :bar
in my path :
foo/baz-123/qux-quux/test
The controller returns the following parameters : {foo: 'foo', baz: 'baz-123', qux: 'qux-quux', slug: 'test'}
Now, the specs changed, and I need to accept alphanumeric characters in the baz
constraint, so I did this :
get ':foo(/:bar)/:baz(/:qux)/:slug', to: 'application#show', constraints: { baz: /. ?\-[a-z0-9] / }, as: test
With this configuration, parameters mapping starts behaving differently. The same path foo/baz-123/qux-quux/test
returns the following parameters in the controller :
{foo: 'foo', bar: 'baz-123', baz: 'qux-quux', slug: 'test'}
I thought that it's happening because qux-quux
matches the constraint too, so I changed it to /. ?\-(?=.*\d )[a-z0-9] /
to ensure that the :baz
parameter needs at least one digit in the second part after the -
.
However, the controller still returns the same wrong mapping even if qux-quxx
does not match the constraint.
How does Rails map the parameters in case of multiple optional segments ?
CodePudding user response:
Your route path :foo(/:bar)/:baz(/:qux)/:slug
contains 5 segments, 3 mandatory segments and 2 optional segments, so the request path will be validated iff:
- it contains enough 5 segments
- it contains 3 segments, in this case, obviously those are 3 mandatory parts
- it contains 4 segments, here there're 2 valid cases:
:foo/:bar/:baz/:slug
,:foo/:baz/:qux/:slug
.
When you set the constraint for a segment, the matcher will check whether the segment is matched with the constraint or not. As you can see with first 2 cases, the matcher know where the segment :baz
is, however in case of 4 segments, the matcher have to choose one of 2 valid cases, so it confuses, hence it will check segments one by one, and pick the last one matched.
/. ?\-[a-z0-9] / =~ "baz-123" # matched
/. ?\-[a-z0-9] / =~ "qux-quux" # matched <-- the last one
{foo: 'foo', bar: 'baz-123', baz: 'qux-quux', slug: 'test'}
I guess the constraint segments are set higher priority than the others, hence they be checked first, after that the others is checked base on constraint segments position, in your case, after the segment :baz
is verified, the segment on its left hand will be the :bar
and the :slug
segment is on the right hand (the optional segment :qux
will be ignored).
When you change the constraint to /. ?\-(?=.*\d )[a-z0-9] /
/. ?\-(?=.*\d )[a-z0-9] / =~ "baz-123" # matched <-- baz
/. ?\-(?=.*\d )[a-z0-9] / =~ "qux-quux" # not matched
# so the params should be (the `:bar` will be ignored)
{foo: 'foo', baz: 'baz-123', qux: 'qux-quux', slug: 'test'}
In my opinion, it's better to setup the constraint for the segment :bar
also if there's any different pattern between :bar
and :baz
, so the matcher will check one by one and will not be confuse anymore.
get ':foo(/:bar)/:baz(/:qux)/:slug', to: 'application#show', constraints: {
bar: /.*/,
baz: /. ?\-(?=.*\d )[a-z0-9] /
}, as: test
Another approach, i think, you could set the default value for the segment :bar
, and in your controller you could handle that default value, so now there's not any case the matcher will be confused.
One more thing, your last regexp /. ?\-(?=.*\d )[a-z0-9] /
is greedy
, this will swallow all the path without respect the splash /
, hence the path foo/bar/baz-x/test
will failed but the path foo/bar/baz-x/x1x
will passed since there's a digit after the character -
. You could replace the greedy part (?=.*\d )
by (?=[^\/]*\d )
to fix it. So be careful with the regexp constraints.