{"id":13483822,"url":"https://github.com/soveran/toro","last_synced_at":"2025-12-30T00:21:22.910Z","repository":{"id":52507565,"uuid":"63156840","full_name":"soveran/toro","owner":"soveran","description":"Tree oriented routing","archived":false,"fork":false,"pushed_at":"2024-01-27T08:14:34.000Z","size":40,"stargazers_count":147,"open_issues_count":0,"forks_count":5,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-03-22T15:50:02.215Z","etag":null,"topics":["api","fast","lesscode","routing","simple","web"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soveran.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":"CONTRIBUTING","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-07-12T12:40:21.000Z","updated_at":"2024-10-13T06:48:21.000Z","dependencies_parsed_at":"2024-01-27T09:22:43.344Z","dependency_job_id":"5fc17981-d451-48fd-a042-24cbe2cf5d5f","html_url":"https://github.com/soveran/toro","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Ftoro","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Ftoro/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Ftoro/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Ftoro/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soveran","download_url":"https://codeload.github.com/soveran/toro/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245871682,"owners_count":20686246,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","fast","lesscode","routing","simple","web"],"created_at":"2024-07-31T17:01:15.649Z","updated_at":"2025-12-30T00:21:22.879Z","avatar_url":"https://github.com/soveran.png","language":"Crystal","readme":"# Toro\n\n![Toro](http://files.soveran.com/toro/img/toro.png)\n\n![CI](https://github.com/soveran/toro/workflows/Crystal%20CI/badge.svg)\n\nTree Oriented Routing\n\n## Usage\n\nHere's a `hello world` app that you can copy and paste to get a\nsense of how Toro works:\n\n```crystal\nrequire \"toro\"\n\nclass App \u003c Toro::Router\n  def routes\n    get do\n      text \"hello world\"\n    end\n  end\nend\n\nApp.run do |server|\n  server.listen \"0.0.0.0\", 8080\nend\n```\n\nSave it to a file called `hello_world.cr` and run it with\n`crystal run hello_world.cr`. Then access your `hello world` application with\nyour browser, or simply by calling `curl http://localhost:8080/` from the\ncommand line.\n\nWhat follows is an example that showcases some basic routing features:\n\n```crystal\nrequire \"toro\"\n\nclass App \u003c Toro::Router\n\n  # You must define the `routes` methods. It will be the\n  # entry point to your web application.\n  def routes\n\n    # The `get` matcher will execute the block when two conditions\n    # are met: the `REQUEST_METHOD` is equal to \"GET\", and there are\n    # no more path segments to match. In this case, as we haven't\n    # consumed any path segment, the only way for this block to run\n    # would be to have a \"GET\" request to \"/\". Check the API section\n    # to see all available matchers.\n    get do\n\n      # The text method sets the Content-Type to \"text/plain\", and\n      # prints the string to the response.\n      text \"hello world\"\n    end\n\n    # A `String` matcher will run the block only if its content is equal\n    # to the next segment in the current path. In this example, it will\n    # match the request if the first segment is equal to \"users\".\n    # You can always inspect the current path by looking at `path.curr`.\n    on \"users\" do\n\n      # If we get here it's because the previous matcher succeeded. It\n      # means we were able to consume a segment off the current path. More\n      # specifically, we consumed the \"users\" segment, and if we now\n      # inspect the `path.prev` string we will find its value is \"/users\".\n      #\n      # With the next matcher we want to capture a segment. Let's say a\n      # request is made to \"/users/42\". When we arrive at this point, this\n      # symbol will match the number \"42\" and store it in the inbox.\n      on :id do\n\n        # If there are no more segments in the request path and if the\n        # request method is \"GET\", this block will run.\n        get do\n\n          # Now, `inbox[:id]` has the value \"42\". The templates have access\n          # to the inbox and to any other variables defined here.\n          #\n          # The `html` macro expects a path to a template. It automatically\n          # appends the `.ecr` extension, which stands for Embedded Crystal\n          # and is part of the standard library. It also sets the content\n          # type to \"text/html\". For the html example to work, you need to\n          # create the file ./views/users/show.ecr with the following content:\n          #\n          #   hello user \u003c%= inbox[:id] %\u003e\n          #\n          #\n          # Once you have created the file, uncomment the line below.\n          #\n          # html \"views/users/show\"\n\n\n          # As a placeholder, the following directive renders the same message\n          # as plain text. Once you have the HTML template in place, you can\n          # comment or remove both this comment and the `text` directive.\n          #\n          text \"hello user #{inbox[:id]}\"\n        end\n      end\n    end\n\n    # The `default` matcher always succeeds, but it doesn't mean the program's\n    # flow will always reach this point. Once a matcher succeeds and runs a\n    # block, the control is never returned. There's an implicit return at the\n    # end of every block, which stops the processing of the request and\n    # returns the response immediately.\n    #\n    # This route will match all the requests that don't have \"users\" as the\n    # first segment (because of the previous matcher), and it will pass the\n    # control to the `Guests` application, which has to be an instance of\n    # `Toro::Router`. This illustrates how you can compose your applications\n    # and split the logic among different routers.\n    default do\n      mount Guests\n    end\n  end\nend\n\n# This is another Toro application. You can mount apps on top of other Toro\n# in order to achieve a modular design.\nclass Guests \u003c Toro::Router\n  def routes\n    on \"about\" do\n      get do\n        text \"about this site\"\n      end\n    end\n  end\nend\n\n# Start the app on port 8080.\nApp.run do |server|\n  server.listen \"0.0.0.0\", 8080\nend\n```\n\nOnce you have this application running, try the requests below:\n\n```shell\n$ curl http://localhost:8080/\n$ curl http://localhost:8080/about\n$ curl http://localhost:8080/users/42\n```\n\nThe routes are evaluated in a sandbox where the following methods\nare available: `context`, `path`, `inbox`, `mount`, `basic_auth`,\n`root`, `root?`, `default`, `on`, `get`, `put`, `head`, `post`,\n`patch`, `delete`, `options`, `text`, `html`, `json`, `write` and\n`render`.\n\n## API\n\n`context`: Environment variables for the request.\n\n`path`: Helper object that tracks the previous and current path.\n\n`inbox`: Hash with captures and potentially other variables local\nto the request.\n\n`mount`: Mounts a sub app.\n\n`basic_auth`: Yields a username and password from the Authorization\nheader, and returns whatever the block returns or nil.\n\n`root?`: Returns true if the path yet to be consumed is empty.\n\n`root`: Receives a block and calls it only if `root?` is true.\n\n`default`: Receives a block that will be executed inconditionally.\n\n`on`: Receives a value to be matched, and a block that will be\nexecuted only if the request is matched.\n\n`get`: Receives a block and calls it only if `root?` and `get?` are\ntrue.\n\n`put`: Receives a block and calls it only if `root?` and `put?` are\ntrue.\n\n`head`: Receives a block and calls it only if `root?` and `head?`\nare true.\n\n`post`: Receives a block and calls it only if `root?` and `post?`\nare true.\n\n`patch`: Receives a block and calls it only if `root?` and `patch?`\nare true.\n\n`delete`: Receives a block and calls it only if `root?` and `delete?`\nare true.\n\n`options`: Receives a block and calls it only if `root?` and\n`options?` are true.\n\n## Matchers\n\nThe `on` method can receive a `String` to perform path matches; a\n`Symbol` to perform path captures; and a boolean to match any true\nvalues.\n\nEach time `on` matches or captures a segment of the PATH, that part\nof the path is consumed. The current and previous paths can be\nqueried by calling `prev` and `curr` on the `path` object: `path.prev`\nreturns the part of the path already consumed, and `path.curr`\nprovides the current version of the path. Any expression that\nevaluates to a boolean can also be used as a matcher.\n\nCaptures\n--------\n\nWhen a symbol is provided, `on` will try to consume a segment of\nthe path. A segment is defined as any sequence of characters after\na slash and until either another slash or the end of the string.\nThe captured value is stored in the `inbox` hash under the key that\nwas provided as the argument to `on`. For example, after a call to\n`on(:user_id)`, the value for the segment will be stored at\n`inbox[:user_id]`.\n\nSecurity\n--------\n\nThere are no security features built into this routing library. A\nframework using this library should implement the security layer.\n\nRendering\n---------\n\nThe most basic way of returning a string is by calling the method\n`text`. It sets the `Content-Type` header to `text/plain` and writes\nthe passed string to the response. A similar helper is called `html`:\nit takes as an argument the path to an `ECR` template and renders\nits content. A lower level `render` macro is available: it also\nexpects the path to a template, but it doesn't modify the headers.\nThere's a `json` helper method expecting a Crystal generic Object.\nIt will call the `to_json` serializer on the generic object. Please\nnote that you need to require JSON from the standard library in\norder to use this helper (adding `require \"json\"` to your app should\nsuffice). The lower level `write` method writes a string to the\nresponse object. It is used internally by `text` and `json`.\n\nRunning the server\n------------------\n\nIf `App` is an instance of `Toro`, then you can start the server by\ncalling `App.run`. It yields an instance of `HTTP://Server` that you\ncan configure:\n\nFor example, you can start the server on port 80:\n\n```crystal\nApp.run do |server|\n  server.listen \"0.0.0.0\", 80\nend\n```\n\nThe following example shows how to configure SSL certificates:\n\n```crystal\nApp.run do |server|\n  ssl = OpenSSL::SSL::Context::Server.new\n  ssl.private_key = \"path/to/private_key\"\n  ssl.certificate_chain = \"path/to/certificate_chain\"\n  server.tls = ssl\n  server.listen \"0.0.0.0\", 443\nend\n```\n\nRefer to Crystal's documentation for more options.\n\nStatus codes\n------------\n\nThe default status code is `404`. It can be changed and queried\nwith the `status` method:\n\n```crystal\n  status\n  #=\u003e 404\n\n  status 200\n\n  status\n  #=\u003e 200\n```\n\nWhen a request method matcher succeeds, the status code for the\nrequest is changed to `200`.\n\nBasic Auth\n----------\n\nThe `basic_auth` method checks the `Authentication` header and, if\npresent, yields to the block the values for username and password.\n\nHere's an example of how you can use it:\n\n```crystal\nclass A \u003c Toro::Router\n  def users(user : User)\n    get do\n      text \"Hello #{user.name}\"\n    end\n  end\n\n  def users(user : Nil)\n    get do\n      text \"Hello guest!\"\n    end\n  end\n\n  def routes\n    user = basic_auth do |name, pass|\n      User.authenticate(name, pass)\n    end\n\n    users(user)\n  end\nend\n```\n\nThe example overloads the `users` method so that it can deal both\nwith instances of `User` and with `nil`. The flow of your router\nwill naturally continue in one of those methods. You are free to\ndefine any other methods like `users` in order to split the logic\nof your application.\n\nTo illustrate the `basic_auth` feature we used an imaginary `User`\nclass that responds to the `authenticate` method and returns either\nan instance of `User` or nil.\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  toro:\n    github: soveran/toro\n    branch: master\n```\n","funding_links":[],"categories":["Middlewares","Routing"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Ftoro","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoveran%2Ftoro","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Ftoro/lists"}