require "../spec_helper" # Simple Payload class record Payload module Radix describe Tree do context "a new instance" do it "contains a root placeholder node" do tree = Tree(Symbol).new tree.root.should be_a(Node(Symbol)) tree.root.payload?.should be_falsey tree.root.placeholder?.should be_true end end describe "#add" do context "on a new instance" do it "replaces placeholder with new node" do tree = Tree(Symbol).new tree.add "/abc", :abc tree.root.should be_a(Node(Symbol)) tree.root.placeholder?.should be_false tree.root.payload?.should be_truthy tree.root.payload.should eq(:abc) end end context "shared root" do it "inserts properly adjacent nodes" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/a", :a tree.add "/bc", :bc # / (:root) # +-bc (:bc) # \-a (:a) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("bc") tree.root.children[0].payload.should eq(:bc) tree.root.children[1].key.should eq("a") tree.root.children[1].payload.should eq(:a) end it "inserts nodes with shared parent" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/abc", :abc tree.add "/axyz", :axyz # / (:root) # +-a # +-xyz (:axyz) # \-bc (:abc) tree.root.children.size.should eq(1) tree.root.children[0].key.should eq("a") tree.root.children[0].children.size.should eq(2) tree.root.children[0].children[0].key.should eq("xyz") tree.root.children[0].children[1].key.should eq("bc") end it "inserts multiple parent nodes" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/admin/users", :users tree.add "/admin/products", :products tree.add "/blog/tags", :tags tree.add "/blog/articles", :articles # / (:root) # +-admin/ # | +-products (:products) # | \-users (:users) # | # +-blog/ # +-articles (:articles) # \-tags (:tags) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("admin/") tree.root.children[0].payload?.should be_falsey tree.root.children[0].children[0].key.should eq("products") tree.root.children[0].children[1].key.should eq("users") tree.root.children[1].key.should eq("blog/") tree.root.children[1].payload?.should be_falsey tree.root.children[1].children[0].key.should eq("articles") tree.root.children[1].children[0].payload?.should be_truthy tree.root.children[1].children[1].key.should eq("tags") tree.root.children[1].children[1].payload?.should be_truthy end it "inserts multiple nodes with mixed parents" do tree = Tree(Symbol).new tree.add "/authorizations", :authorizations tree.add "/authorizations/:id", :authorization tree.add "/applications", :applications tree.add "/events", :events # / # +-events (:events) # +-a # +-uthorizations (:authorizations) # | \-/:id (:authorization) # \-pplications (:applications) tree.root.children.size.should eq(2) tree.root.children[1].key.should eq("a") tree.root.children[1].children.size.should eq(2) tree.root.children[1].children[0].payload.should eq(:authorizations) tree.root.children[1].children[1].payload.should eq(:applications) end it "supports insertion of mixed routes out of order" do tree = Tree(Symbol).new tree.add "/user/repos", :my_repos tree.add "/users/:user/repos", :user_repos tree.add "/users/:user", :user tree.add "/user", :me # /user (:me) # +-/repos (:my_repos) # \-s/:user (:user) # \-/repos (:user_repos) tree.root.key.should eq("/user") tree.root.payload?.should be_truthy tree.root.payload.should eq(:me) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("/repos") tree.root.children[1].key.should eq("s/:user") tree.root.children[1].payload.should eq(:user) tree.root.children[1].children[0].key.should eq("/repos") end end context "mixed payloads" do it "allows node with different payloads" do payload1 = Payload.new payload2 = Payload.new tree = Tree(Payload | Symbol).new tree.add "/", :root tree.add "/a", payload1 tree.add "/bc", payload2 # / (:root) # +-bc (payload2) # \-a (payload1) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("bc") tree.root.children[0].payload.should eq(payload2) tree.root.children[1].key.should eq("a") tree.root.children[1].payload.should eq(payload1) end end context "dealing with unicode" do it "inserts properly adjacent parent nodes" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/日本語", :japanese tree.add "/素晴らしい", :amazing # / (:root) # +-素晴らしい (:amazing) # \-日本語 (:japanese) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("素晴らしい") tree.root.children[1].key.should eq("日本語") end it "inserts nodes with shared parent" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/日本語", :japanese tree.add "/日本は難しい", :japanese_is_difficult # / (:root) # \-日本語 (:japanese) # \-日本は難しい (:japanese_is_difficult) tree.root.children.size.should eq(1) tree.root.children[0].key.should eq("日本") tree.root.children[0].children.size.should eq(2) tree.root.children[0].children[0].key.should eq("は難しい") tree.root.children[0].children[1].key.should eq("語") end end context "dealing with duplicates" do it "does not allow same path be defined twice" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/abc", :abc expect_raises Tree::DuplicateError do tree.add "/", :other end tree.root.children.size.should eq(1) end end context "dealing with catch all and named parameters" do it "prioritizes nodes correctly" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/*filepath", :all tree.add "/products", :products tree.add "/products/:id", :product tree.add "/products/:id/edit", :edit tree.add "/products/featured", :featured # / (:all) # +-products (:products) # | \-/ # | +-featured (:featured) # | \-:id (:product) # | \-/edit (:edit) # \-*filepath (:all) tree.root.children.size.should eq(2) tree.root.children[0].key.should eq("products") tree.root.children[0].children[0].key.should eq("/") nodes = tree.root.children[0].children[0].children nodes.size.should eq(2) nodes[0].key.should eq("featured") nodes[1].key.should eq(":id") nodes[1].children[0].key.should eq("/edit") tree.root.children[1].key.should eq("*filepath") end it "does not split named parameters across shared key" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/:category", :category tree.add "/:category/:subcategory", :subcategory # / (:root) # +-:category (:category) # \-/:subcategory (:subcategory) tree.root.children.size.should eq(1) tree.root.children[0].key.should eq(":category") # inner children tree.root.children[0].children.size.should eq(1) tree.root.children[0].children[0].key.should eq("/:subcategory") end it "does allow same named parameter in different order of insertion" do tree = Tree(Symbol).new tree.add "/members/:id/edit", :member_edit tree.add "/members/export", :members_export tree.add "/members/:id/videos", :member_videos # /members/ # +-export (:members_export) # \-:id/ # +-videos (:members_videos) # \-edit (:members_edit) tree.root.key.should eq("/members/") tree.root.children.size.should eq(2) # first level children nodes tree.root.children[0].key.should eq("export") tree.root.children[1].key.should eq(":id/") # inner children nodes = tree.root.children[1].children nodes[0].key.should eq("videos") nodes[1].key.should eq("edit") end it "does not allow different named parameters sharing same level" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/:post", :post expect_raises Tree::SharedKeyError do tree.add "/:category/:post", :category_post end end end end describe "#find" do context "a single node" do it "does not find when using different path" do tree = Tree(Symbol).new tree.add "/about", :about result = tree.find "/products" result.found?.should be_false end it "finds when key and path matches" do tree = Tree(Symbol).new tree.add "/about", :about result = tree.find "/about" result.found?.should be_true result.payload?.should be_truthy result.payload.should eq(:about) end it "finds when path contains trailing slash" do tree = Tree(Symbol).new tree.add "/about", :about result = tree.find "/about/" result.found?.should be_true end it "finds when key contains trailing slash" do tree = Tree(Symbol).new tree.add "/about/", :about result = tree.find "/about" result.found?.should be_true result.payload.should eq(:about) end end context "nodes with shared parent" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/abc", :abc tree.add "/axyz", :axyz result = tree.find("/abc") result.found?.should be_true result.payload.should eq(:abc) end it "finds matching path across separator" do tree = Tree(Symbol).new tree.add "/products", :products tree.add "/product/new", :product_new result = tree.find("/products") result.found?.should be_true result.payload.should eq(:products) end it "finds matching path across parents" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/admin/users", :users tree.add "/admin/products", :products tree.add "/blog/tags", :tags tree.add "/blog/articles", :articles result = tree.find("/blog/tags/") result.found?.should be_true result.payload.should eq(:tags) end it "do not find when lookup for non-root key" do tree = Tree(Symbol).new tree.add "/prefix/", :prefix tree.add "/prefix/foo", :foo result = tree.find "/foo" result.found?.should be_false end end context "unicode nodes with shared parent" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/日本語", :japanese tree.add "/日本日本語は難しい", :japanese_is_difficult result = tree.find("/日本日本語は難しい/") result.found?.should be_true result.payload.should eq(:japanese_is_difficult) end end context "dealing with catch all" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/*filepath", :all tree.add "/about", :about result = tree.find("/src/file.png") result.found?.should be_true result.payload.should eq(:all) end it "returns catch all in parameters" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/*filepath", :all tree.add "/about", :about result = tree.find("/src/file.png") result.found?.should be_true result.params.has_key?("filepath").should be_true result.params["filepath"].should eq("src/file.png") end it "returns optional catch all after slash" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/search/*extra", :extra result = tree.find("/search") result.found?.should be_true result.params.has_key?("extra").should be_true result.params["extra"].empty?.should be_true end it "returns optional catch all by globbing" do tree = Tree(Symbol).new tree.add "/members*trailing", :members_catch_all result = tree.find("/members") result.found?.should be_true result.params.has_key?("trailing").should be_true result.params["trailing"].empty?.should be_true end it "does not find when catch all is not full match" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/search/public/*query", :search result = tree.find("/search") result.found?.should be_false end it "does not find when path search has been exhausted" do tree = Tree(Symbol).new tree.add "/members/*trailing", :members_catch_all result = tree.find("/members2") result.found?.should be_false end it "does prefer specific path over catch all if both are present" do tree = Tree(Symbol).new tree.add "/members", :members tree.add "/members*trailing", :members_catch_all result = tree.find("/members") result.found?.should be_true result.payload.should eq(:members) end it "does prefer catch all over specific key with partially shared key" do tree = Tree(Symbol).new tree.add "/orders/*anything", :orders_catch_all tree.add "/orders/closed", :closed_orders result = tree.find("/orders/cancelled") result.found?.should be_true result.payload.should eq(:orders_catch_all) result.params.has_key?("anything").should be_true result.params["anything"].should eq("cancelled") end it "does prefer root catch all over specific partially shared key" do tree = Tree(Symbol).new tree.add "/*anything", :root_catch_all tree.add "/robots.txt", :robots tree.add "/resources", :resources result = tree.find("/reviews") result.found?.should be_true result.payload.should eq(:root_catch_all) result.params.has_key?("anything").should be_true result.params["anything"].should eq("reviews") end end context "dealing with named parameters" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/products", :products tree.add "/products/:id", :product tree.add "/products/:id/edit", :edit result = tree.find("/products/10") result.found?.should be_true result.payload.should eq(:product) end it "does not find partial matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/products", :products tree.add "/products/:id/edit", :edit result = tree.find("/products/10") result.found?.should be_false end it "returns named parameters in result" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/products", :products tree.add "/products/:id", :product tree.add "/products/:id/edit", :edit result = tree.find("/products/10/edit") result.found?.should be_true result.params.has_key?("id").should be_true result.params["id"].should eq("10") end it "returns unicode values in parameters" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/language/:name", :language tree.add "/language/:name/about", :about result = tree.find("/language/日本語") result.found?.should be_true result.params.has_key?("name").should be_true result.params["name"].should eq("日本語") end it "does prefer specific path over named parameters one if both are present" do tree = Tree(Symbol).new tree.add "/tag-edit/:tag", :edit_tag tree.add "/tag-edit2", :alternate_tag_edit result = tree.find("/tag-edit2") result.found?.should be_true result.payload.should eq(:alternate_tag_edit) end it "does prefer named parameter over specific key with partially shared key" do tree = Tree(Symbol).new tree.add "/orders/:id", :specific_order tree.add "/orders/closed", :closed_orders result = tree.find("/orders/10") result.found?.should be_true result.payload.should eq(:specific_order) result.params.has_key?("id").should be_true result.params["id"].should eq("10") end end context "dealing with multiple named parameters" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/:section/:page", :static_page result = tree.find("/about/shipping") result.found?.should be_true result.payload.should eq(:static_page) end it "returns named parameters in result" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/:section/:page", :static_page result = tree.find("/about/shipping") result.found?.should be_true result.params.has_key?("section").should be_true result.params["section"].should eq("about") result.params.has_key?("page").should be_true result.params["page"].should eq("shipping") end end context "dealing with both catch all and named parameters" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/", :root tree.add "/*filepath", :all tree.add "/products", :products tree.add "/products/:id", :product tree.add "/products/:id/edit", :edit tree.add "/products/featured", :featured result = tree.find("/products/1000") result.found?.should be_true result.payload.should eq(:product) result = tree.find("/admin/articles") result.found?.should be_true result.payload.should eq(:all) result.params["filepath"].should eq("admin/articles") result = tree.find("/products/featured") result.found?.should be_true result.payload.should eq(:featured) result.payload.should eq(:featured) end end context "dealing with named parameters and shared key" do it "finds matching path" do tree = Tree(Symbol).new tree.add "/one/:id", :one tree.add "/one-longer/:id", :two result = tree.find "/one-longer/10" result.found?.should be_true result.payload.should eq(:two) result.params["id"].should eq("10") end end end end end