Home > Back-end >  Rails route optional segments mapping issue
Rails route optional segments mapping issue

Time:10-20

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.

  • Related