Skip to content

Commit

Permalink
#52 - Make it possible to define route paths prefixed with locales
Browse files Browse the repository at this point in the history
  • Loading branch information
ellmetha committed Dec 25, 2024
1 parent cd2f881 commit 931a26c
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 5 deletions.
17 changes: 17 additions & 0 deletions spec/ext/marten/routing/map.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Marten::Routing::Map
def exposed_localizing?
@localizing
end

def exposed_localizing=(value)
@localizing = value
end

def exposed_root?
@root
end

def exposed_root=(value)
@root = value
end
end
87 changes: 87 additions & 0 deletions spec/marten/routing/map_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
require "./spec_helper"

describe Marten::Routing::Match do
describe "#localized" do
it "raises if the map is not the root one" do
map = Marten::Routing::Map.new

expect_raises(Marten::Routing::Errors::InvalidRouteMap, "Cannot define localized routes in a non-root map") do
map.localized do
path t("routes.blog"), Marten::Handlers::Base, name: "blog"
end
end
end

it "allows to define localized routes" do
map = Marten::Routing::Map.new
map.exposed_root = true

map.localized do
path t("routes.foo_bar"), Marten::Handlers::Base, name: "foo_bar"
end

map.rules.size.should eq 1
map.rules.first.should be_a Marten::Routing::Rule::Localized

map.reverse("foo_bar").should eq "/en/foo/bar"

I18n.with_locale(:fr) do
map.reverse("foo_bar").should eq "/fr/foo-french/bar-french"
end
end

it "allows to define localized routes without the default locale prefix" do
map = Marten::Routing::Map.new
map.exposed_root = true

map.localized(prefix_default_locale: false) do
path t("routes.foo_bar"), Marten::Handlers::Base, name: "foo_bar"
end

map.rules.size.should eq 1
map.rules.first.should be_a Marten::Routing::Rule::Localized

map.reverse("foo_bar").should eq "/foo/bar"

I18n.with_locale(:fr) do
map.reverse("foo_bar").should eq "/fr/foo-french/bar-french"
end
end

it "is consistent with multiple invocations" do
map = Marten::Routing::Map.new
map.exposed_root = true

map.localized do
path t("routes.foo_bar"), Marten::Handlers::Base, name: "foo_bar"

map.exposed_localizing?.should be_true

map.localized do
path t("routes.foo_bar"), Marten::Handlers::Base, name: "other_foo_bar"

map.exposed_localizing?.should be_true
end
end

map.exposed_localizing?.should be_false
end
end

describe "#path" do
it "can be used with a translated path" do
map = Marten::Routing::Map.new
Expand Down Expand Up @@ -36,6 +103,26 @@ describe Marten::Routing::Match do
map.path("/bis", Marten::Handlers::Base, name: "home")
end
end

context "while localizing" do
it "allows to define localized routes" do
map = Marten::Routing::Map.new
map.exposed_root = true

map.localized do
path t("routes.foo_bar"), Marten::Handlers::Base, name: "foo_bar"
path t("routes.foo_bar_with_args"), Marten::Handlers::Base, name: "foo_bar_with_args"
end

map.reverse("foo_bar").should eq "/en/foo/bar"
map.reverse("foo_bar_with_args", param1: 42, param2: "hello-world").should eq "/en/foo/42/bar/hello-world"

I18n.with_locale(:fr) do
map.reverse("foo_bar").should eq "/fr/foo-french/bar-french"
map.reverse("foo_bar_with_args", param1: 42, param2: "hello-world").should eq "/fr/foo/42/bar/hello-world"
end
end
end
end

describe "#resolve" do
Expand Down
50 changes: 50 additions & 0 deletions spec/marten/routing/reverser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,55 @@ describe Marten::Routing::Reverser do
)
reverser.reverse({param1: "hello-world"}.to_h).should be_nil
end

it "returns the interpolated path for matching parameters when prefixed and localized paths are used" do
reverser = Marten::Routing::Reverser.new(
"path:name",
{
"en" => "/this-is-a-test/%{param1}/xyz/%{param2}",
"fr" => "/ceci-est-un-test/%{param1}/xyz/%{param2}",
} of String? => String,
{
"param1" => Marten::Routing::Parameter.registry["slug"],
"param2" => Marten::Routing::Parameter.registry["int"],
}
)

reverser.prefix_locales = true
reverser.prefix_default_locale = true

I18n.with_locale("en") do
reverser.reverse({param1: "hello-world", param2: 42}.to_h).should eq "/en/this-is-a-test/hello-world/xyz/42"
end

I18n.with_locale("fr") do
reverser.reverse({param1: "hello-world", param2: 42}.to_h).should eq "/fr/ceci-est-un-test/hello-world/xyz/42"
end
end

it "does not prefix the path if the locale is the default locale and the prefix_default_locale option is false" do
reverser = Marten::Routing::Reverser.new(
"path:name",
{
"en" => "/this-is-a-test/%{param1}/xyz/%{param2}",
"fr" => "/ceci-est-un-test/%{param1}/xyz/%{param2}",
} of String? => String,
{
"param1" => Marten::Routing::Parameter.registry["slug"],
"param2" => Marten::Routing::Parameter.registry["int"],
}
)

reverser.prefix_locales = true
reverser.prefix_default_locale = false

I18n.with_locale("en") do
reverser.reverse({param1: "hello-world", param2: 42}.to_h).should eq "/this-is-a-test/hello-world/xyz/42"
end

I18n.with_locale("fr") do
reverser.reverse({param1: "hello-world", param2: 42}.to_h).should eq "/fr/ceci-est-un-test/hello-world/xyz/42"
end
end
end
end
161 changes: 161 additions & 0 deletions spec/marten/routing/rule/localized_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
require "./spec_helper"

describe Marten::Routing::Rule::Localized do
describe "#name" do
it "raises NotImplementedError" do
rule = Marten::Routing::Rule::Localized.new

expect_raises(NotImplementedError, "Localized rules don't provide names") do
rule.name
end
end
end

describe "#resolve" do
it "returns the expected match if the path starts with the current locale prefix and if the rule matches" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new
rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)

I18n.with_locale("en") do
rule.resolve("/en/home/xyz/my-slug/count/42/display").should be_a Marten::Routing::Match
end

I18n.with_locale("fr") do
rule.resolve("/fr/home/xyz/my-slug/count/42/display").should be_a Marten::Routing::Match
end
end

it "returns the expected match when the default locale is not prefixed" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new(prefix_default_locale: false)

rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)

I18n.with_locale("en") do
rule.resolve("/home/xyz/my-slug/count/42/display").should be_a Marten::Routing::Match
end

I18n.with_locale("fr") do
rule.resolve("/fr/home/xyz/my-slug/count/42/display").should be_a Marten::Routing::Match
end
end

it "returns nil if the path does not start with the current locale prefix" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new
rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)

I18n.with_locale("fr") do
rule.resolve("/home/xyz/my-slug/count/42/display").should be_nil
rule.resolve("/en/home/xyz/my-slug/count/42/display").should be_nil
end
end

it "returns nil if the rule does not match" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new
rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)

I18n.with_locale("en") do
rule.resolve("/en/home/xyz/my-slug/count/not-a-number/display").should be_nil
end

I18n.with_locale("fr") do
rule.resolve("/fr/home/xyz/my-slug/count/not-a-number/display").should be_nil
end
end
end

describe "#reversers" do
it "returns the expected reversers when the default locale is prefixed" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new(prefix_default_locale: true)

rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)
rule.rules << Marten::Routing::Rule::Path.new(
"/other/path/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "other_path"
)

reversers = rule.exposed_reversers

reversers.size.should eq 2

reversers[0].prefix_locales?.should be_true
reversers[0].prefix_default_locale?.should be_true
reversers[0].name.should eq "home_xyz"
reversers[0].path_for_interpolation.should eq "/home/xyz/%{sid}/count/%{number}/display"
reversers[0].parameters.size.should eq 2
reversers[0].parameters["sid"].should be_a Marten::Routing::Parameter::Slug
reversers[0].parameters["number"].should be_a Marten::Routing::Parameter::Integer

reversers[1].prefix_locales?.should be_true
reversers[1].prefix_default_locale?.should be_true
reversers[1].name.should eq "other_path"
reversers[1].path_for_interpolation.should eq "/other/path/%{sid}/count/%{number}/display"
reversers[1].parameters.size.should eq 2
reversers[1].parameters["sid"].should be_a Marten::Routing::Parameter::Slug
reversers[1].parameters["number"].should be_a Marten::Routing::Parameter::Integer
end

it "returns the expected reversers when the default locale is not prefixed" do
rule = Marten::Routing::Rule::LocalizedSpec::TestLocalizedRule.new(prefix_default_locale: false)

rule.rules << Marten::Routing::Rule::Path.new(
"/home/xyz/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "home_xyz"
)
rule.rules << Marten::Routing::Rule::Path.new(
"/other/path/<sid:slug>/count/<number:int>/display",
Marten::Handlers::Base,
name: "other_path"
)

reversers = rule.exposed_reversers

reversers.size.should eq 2

reversers[0].prefix_locales?.should be_true
reversers[0].prefix_default_locale?.should be_false
reversers[0].name.should eq "home_xyz"
reversers[0].path_for_interpolation.should eq "/home/xyz/%{sid}/count/%{number}/display"
reversers[0].parameters.size.should eq 2
reversers[0].parameters["sid"].should be_a Marten::Routing::Parameter::Slug
reversers[0].parameters["number"].should be_a Marten::Routing::Parameter::Integer

reversers[1].prefix_locales?.should be_true
reversers[1].prefix_default_locale?.should be_false
reversers[1].name.should eq "other_path"
reversers[1].path_for_interpolation.should eq "/other/path/%{sid}/count/%{number}/display"
reversers[1].parameters.size.should eq 2
reversers[1].parameters["sid"].should be_a Marten::Routing::Parameter::Slug
reversers[1].parameters["number"].should be_a Marten::Routing::Parameter::Integer
end
end
end

module Marten::Routing::Rule::LocalizedSpec
class TestLocalizedRule < Marten::Routing::Rule::Localized
def exposed_reversers
reversers
end
end
end
7 changes: 7 additions & 0 deletions spec/marten_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ describe Marten do
end
end

describe "#routes" do
it "returns the main routes map with the root flag set to true" do
Marten.routes.should be_a Marten::Routing::Map
Marten.routes.exposed_root?.should be_true
end
end

describe "#setup" do
context "with root path" do
around_each do |t|
Expand Down
4 changes: 3 additions & 1 deletion src/marten.cr
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ module Marten
# This method returns the main routes map, initialized according to the routes configuration and allowing to perform
# reverse URL resolutions.
def self.routes
@@routes ||= Routing::Map.new
@@routes ||= Routing::Map.new.tap do |map|
map.root = true
end
end

# Returns the settings of the application.
Expand Down
3 changes: 3 additions & 0 deletions src/marten/routing/errors.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ module Marten
# Represents an error raised when a route cannot be reversed.
class NoReverseMatch < Exception; end

# Represents an error raised when an invalid route map is defined.
class InvalidRouteMap < Exception; end

# Represents an error raised when an invalid route name is defined.
class InvalidRuleName < Exception; end

Expand Down
Loading

0 comments on commit 931a26c

Please sign in to comment.