├── .gems ├── CHANGELOG ├── CONTRIBUTING ├── LICENSE ├── README.md ├── lib └── syro.rb ├── makefile ├── syro.gemspec └── test ├── all.rb └── helper.rb /.gems: -------------------------------------------------------------------------------- 1 | cutest -v 1.2.3 2 | seg -v 1.2.0 3 | rack -v 2.2.3 4 | rack-test -v 1.1.0 5 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 3.0.0 2 | 3 | * Change how status codes are set. 4 | * Change how the Content-Type is set. 5 | * Add Content-Type helpers to Syro::Response. 6 | 7 | 2.2.0 8 | 9 | * Change gemspec to allow Rack 2. 10 | 11 | 2.1.2 12 | 13 | * Always set default content type if body is set. 14 | 15 | 2.1.1 16 | 17 | * Change gemspec to accept pre-release versions of Rack 2 18 | 19 | 2.1.0 20 | 21 | * Add matchers for head and options 22 | 23 | 2.0.0 24 | 25 | * No changes since 2.0.0.rc1 26 | 27 | 2.0.0.rc1 28 | 29 | * Add consume and capture primitives 30 | 31 | * Add default matcher 32 | 33 | * Revert yield of captured value (was added in 1.0.0) 34 | 35 | 1.1.1 36 | 37 | * Small internal refactoring 38 | 39 | 1.1.0 40 | 41 | * Move Deck methods to the Deck::API module 42 | 43 | * Add support for custom response and request classes 44 | 45 | 1.0.0 46 | 47 | * Yield captures as an alternative API 48 | 49 | * Add support for default headers in response 50 | 51 | 0.0.8 52 | 53 | * Reset PATH_INFO and SCRIPT_NAME after running a subapp 54 | 55 | 0.0.7 56 | 57 | * Change run so that it accepts Rack applications 58 | 59 | 0.0.6 60 | 61 | * Add put request method 62 | 63 | 0.0.5 64 | 65 | * Add explicit Rack version dependency (@luislavena) 66 | 67 | 0.0.4 68 | 69 | * Add concept of Decks 70 | 71 | 0.0.3 72 | 73 | * Remove request method overriding (@larrylv) 74 | 75 | 0.0.2 76 | 77 | * Defer method comparisons to Rack::Request (@pote) 78 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | This code tries to solve a particular problem with a very simple 2 | implementation. We try to keep the code to a minimum while making 3 | it as clear as possible. The design is very likely finished, and 4 | if some feature is missing it is possible that it was left out on 5 | purpose. That said, new usage patterns may arise, and when that 6 | happens we are ready to adapt if necessary. 7 | 8 | A good first step for contributing is to meet us on IRC and discuss 9 | ideas. We spend a lot of time on #lesscode at freenode, always ready 10 | to talk about code and simplicity. If connecting to IRC is not an 11 | option, you can create an issue explaining the proposed change and 12 | a use case. We pay a lot of attention to use cases, because our 13 | goal is to keep the code base simple. Usually the result of a 14 | conversation is the creation of a different tool. 15 | 16 | Please don't start the conversation with a pull request. The code 17 | should come at last, and even though it may help to convey an idea, 18 | more often than not it draws the attention to a particular 19 | implementation. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Michel Martens 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Syro 2 | ==== 3 | 4 | Simple router for web applications. 5 | 6 | Community 7 | --------- 8 | 9 | Meet us on IRC: [#syro](irc://chat.freenode.net/#syro) on 10 | [freenode.net](http://freenode.net/). 11 | 12 | Description 13 | ----------- 14 | 15 | Syro is a very simple router for web applications. It was created 16 | in the tradition of libraries like [Rum][rum] and [Cuba][cuba], but 17 | it promotes a less flexible usage pattern. The design is inspired 18 | by the way some Cuba applications are architected: modularity is 19 | encouraged and sub-applications can be dispatched without any 20 | significant performance overhead. 21 | 22 | Check the [website][syro] for more information, and follow the 23 | [tutorial][tutorial] for a step by step introduction. 24 | 25 | [rum]: http://github.com/chneukirchen/rum 26 | [cuba]: http://cuba.is 27 | [syro]: http://soveran.github.io/syro/ 28 | [tutorial]: http://files.soveran.com/syro/ 29 | 30 | Usage 31 | ----- 32 | 33 | An example of a modular application would look like this: 34 | 35 | ```ruby 36 | Admin = Syro.new do 37 | get do 38 | res.write "Hello from admin!" 39 | end 40 | end 41 | 42 | App = Syro.new do 43 | on "admin" do 44 | run(Admin) 45 | end 46 | end 47 | ``` 48 | 49 | The block is evaluated in a sandbox where the following methods are 50 | available: `env`, `req`, `res`, `path`, `inbox`, `call`, `run`, 51 | `halt`, `handle`, `finish!`, `consume`, `capture`, `root?` `match`, 52 | `default`, `on`, `root`,`get`, `put`, `head`, `post`, `patch`, 53 | `delete` and `options`. Three other methods are available for 54 | customizations: `default_headers`, `request_class` and `response_class`. 55 | 56 | As a recommendation, user created variables should be instance 57 | variables. That way they won't mix with the API methods defined in 58 | the sandbox. All the internal instance variables defined by Syro 59 | are prefixed by `syro_`, like in `@syro_inbox`. 60 | 61 | API 62 | --- 63 | 64 | `env`: Environment variables for the request. 65 | 66 | `req`: Helper object for accessing the request variables. It's an 67 | instance of `Rack::Request`. 68 | 69 | `res`: Helper object for creating the response. It's an instance 70 | of `Syro::Response`. 71 | 72 | `path`: Helper object that tracks the previous and current path. 73 | 74 | `inbox`: Hash with captures and potentially other variables local 75 | to the request. 76 | 77 | `call`: Entry point for the application. It receives the environment 78 | and optionally an inbox. 79 | 80 | `run`: Runs a sub app, and accepts an inbox as an optional second 81 | argument. 82 | 83 | `halt`: Terminates the request. It receives an array with the 84 | response as per Rack's specification. 85 | 86 | `handle`: Installs a handler for a given status code. It receives 87 | a status code and a block that will be executed from `finish!`. 88 | 89 | `finish!`: Terminates the request by executing any installed handlers 90 | and then halting with the current value of `res.finish`. 91 | 92 | `consume`: Match and consume a path segment. 93 | 94 | `capture`: Match and capture a path segment. The value is stored in 95 | the inbox. 96 | 97 | `root?`: Returns true if the path yet to be consumed is empty. 98 | 99 | `match`: Receives a String, a Symbol or a boolean, and returns true 100 | if it matches the request. 101 | 102 | `default`: Receives a block that will be executed unconditionally. 103 | 104 | `on`: Receives a value to be matched, and a block that will be 105 | executed only if the request is matched. 106 | 107 | `root`: Receives a block and calls it only if `root?` is true. 108 | 109 | `get`: Receives a block and calls it only if `root?` and `req.get?` are 110 | true. 111 | 112 | `put`: Receives a block and calls it only if `root?` and `req.put?` are 113 | true. 114 | 115 | `head`: Receives a block and calls it only if `root?` and `req.head?` 116 | are true. 117 | 118 | `post`: Receives a block and calls it only if `root?` and `req.post?` 119 | are true. 120 | 121 | `patch`: Receives a block and calls it only if `root?` and `req.patch?` 122 | are true. 123 | 124 | `delete`: Receives a block and calls it only if `root?` and `req.delete?` 125 | are true. 126 | 127 | `options`: Receives a block and calls it only if `root?` and 128 | `req.options?` are true. 129 | 130 | Decks 131 | ----- 132 | 133 | The sandbox where the application is evaluated is an instance of 134 | `Syro::Deck`, and it provides the API described earlier. You can 135 | define your own `Deck` and pass it to the `Syro` constructor. All 136 | the methods defined in there will be accessible from your routes. 137 | Here's an example: 138 | 139 | ```ruby 140 | class TextualDeck < Syro::Deck 141 | def text(str) 142 | res[Rack::CONTENT_TYPE] = "text/plain" 143 | res.write(str) 144 | end 145 | end 146 | 147 | App = Syro.new(TextualDeck) do 148 | get do 149 | text("hello world") 150 | end 151 | end 152 | ``` 153 | 154 | The example is simple enough to showcase the concept, but maybe too 155 | simple to be meaningful. The idea is that you can create your own 156 | specialized decks and reuse them in different applications. You can 157 | also define modules and later include them in your decks: for 158 | example, you can write modules for rendering or serializing data, 159 | and then you can combine those modules in your custom decks. 160 | 161 | Examples 162 | -------- 163 | 164 | In the following examples, the response string represents 165 | the request path that was sent. 166 | 167 | ```ruby 168 | App = Syro.new do 169 | get do 170 | res.write "GET /" 171 | end 172 | 173 | post do 174 | res.write "POST /" 175 | end 176 | 177 | on "users" do 178 | on :id do 179 | 180 | # Captured values go to the inbox 181 | @user = User[inbox[:id]] 182 | 183 | get do 184 | res.write "GET /users/42" 185 | end 186 | 187 | put do 188 | res.write "PUT /users/42" 189 | end 190 | 191 | patch do 192 | res.write "PATCH /users/42" 193 | end 194 | 195 | delete do 196 | res.write "DELETE /users/42" 197 | end 198 | end 199 | 200 | get do 201 | res.write "GET /users" 202 | end 203 | 204 | post do 205 | res.write "POST /users" 206 | end 207 | end 208 | end 209 | ``` 210 | 211 | Matches 212 | ------- 213 | 214 | The `on` method can receive a `String` to perform path matches; a 215 | `Symbol` to perform path captures; and a boolean to match any true 216 | values. 217 | 218 | Each time `on` matches or captures a segment of the PATH, that part 219 | of the path is consumed. The current and previous paths can be 220 | queried by calling `prev` and `curr` on the `path` object: `path.prev` 221 | returns the part of the path already consumed, and `path.curr` 222 | provides the current version of the path. 223 | 224 | Any expression that evaluates to a boolean can also be used as a 225 | matcher. For example, a common pattern is to follow some route 226 | only if a user is authenticated. That can be accomplished with 227 | `on(authenticated(User))`. That example assumes there's a method 228 | called `authenticated` that returns true or false depending on 229 | whether or not an instance of `User` is authenticated. As a side 230 | note, [Shield][shield] is a library that provides just that. 231 | 232 | [shield]: https://github.com/cyx/shield 233 | 234 | Captures 235 | -------- 236 | 237 | When a symbol is provided, `on` will try to consume a segment of 238 | the path. A segment is defined as any sequence of characters after 239 | a slash and until either another slash or the end of the string. 240 | The captured value is stored in the `inbox` hash under the key that 241 | was provided as the argument to `on`. For example, after a call to 242 | `on(:user_id)`, the value for the segment will be stored at 243 | `inbox[:user_id]`. When mounting an application called `Users` with 244 | the command `run(Users)`, an inbox can be provided as the second 245 | argument: `run(Users, inbox)`. That allows apps to share previous 246 | captures. 247 | 248 | Status code 249 | ----------- 250 | 251 | By default the status code is set to `404`. If both path and request 252 | method are matched, the status is automatically changed to `200`. 253 | You can change the status code by assigning a number to `res.status`, 254 | for example: 255 | 256 | ```ruby 257 | post do 258 | ... 259 | res.status = 201 260 | end 261 | ``` 262 | 263 | Handlers 264 | -------- 265 | 266 | Status code handlers can be installed with the `handle` command, 267 | which receives a status code and a block to be executed just before 268 | finishing the request. 269 | 270 | By default, if there are no matches in a Syro application the 271 | response is a `404` with an empty body. If we decide to handle the 272 | `404` requests and return a string, we can do as follows: 273 | 274 | ```ruby 275 | App = Syro.new do 276 | handle 404 do 277 | res.text "Not found!" 278 | end 279 | 280 | get do 281 | res.text "Found!" 282 | end 283 | end 284 | ``` 285 | 286 | In this example, a `GET` request to `"/"` will return a status `200` 287 | with the body `"Found!"`. Any other request will return a `404` 288 | with the body `"Not found!"`. 289 | 290 | If a new handler is installed for the same status code, the previous 291 | handler is overwritten. A handler is valid in the current scope and 292 | in all its nested branches. Blocks that end before the handler is 293 | installed are not affected. 294 | 295 | This is a contrived example that shows some edge cases when using handlers: 296 | 297 | ```ruby 298 | App = Syro.new do 299 | on "foo" do 300 | # 404, empty body 301 | end 302 | 303 | handle 404 do 304 | res.text "Not found!" 305 | end 306 | 307 | on "bar" do 308 | # 404, body is "Not found!" 309 | end 310 | 311 | on "baz" do 312 | # 404, body is "Couldn't find baz" 313 | 314 | handle 404 do 315 | res.text "Couldn't find baz" 316 | end 317 | end 318 | end 319 | ``` 320 | 321 | A request to `"/foo"` will return a `404`, because the request 322 | method was not matched. But as the `on "foo"` block ends before the 323 | handler is installed, the result will be a blank screen. On the 324 | other hand, a request to `"/bar"` will return a `404` with the plain 325 | text `"Not found!"`. 326 | 327 | Finally, a request to `"/baz"` will return a `404` with the plain text 328 | `"Couldn't find baz"`, because by the time the `on "baz"` block ends 329 | a new handler is installed, and thus the previous one is overwritten. 330 | 331 | Any status code can be handled this way, even status `200`. In that 332 | case the handler will behave as a filter to be run after each 333 | successful request. 334 | 335 | Content type 336 | ------------ 337 | 338 | There's no default value for the content type header, but there's 339 | a handy way of setting the desired value. 340 | 341 | In order to write the body of the response, the `res.write` method 342 | is used: 343 | 344 | ```ruby 345 | res.write "hello world" 346 | ``` 347 | 348 | It has the drawback of leaving the `Content-Type` header empty. 349 | Three alternative methods are provided, and more can be added by 350 | using custom Decks. 351 | 352 | Setting the Content-Type as `"text/plain"`: 353 | 354 | ```ruby 355 | res.text "hello world" 356 | ``` 357 | 358 | Setting the Content-Type as `"text/html"`: 359 | 360 | ```ruby 361 | res.html "hello world" 362 | ``` 363 | 364 | Setting the Content-Type as `"application/json"`: 365 | 366 | ```ruby 367 | res.json "hello world" 368 | ``` 369 | 370 | Note that aside from writing the response body and setting the value 371 | for the Content-Type header, no encoding or serialization takes 372 | place. If you want to return a JSON encoded response, make sure to 373 | encode the objects yourself (i.e., `res.json JSON.dump(...)`). 374 | 375 | Security 376 | -------- 377 | 378 | There are no security features built into this routing library. A 379 | framework using this library should implement the security layer. 380 | 381 | Rendering 382 | --------- 383 | 384 | There are no rendering features built into this routing library. A 385 | framework that uses this routing library can easily implement helpers 386 | for rendering. 387 | 388 | Middleware 389 | ---------- 390 | 391 | Syro doesn't support Rack middleware out of the box. If you need them, 392 | just use `Rack::Builder`: 393 | 394 | ```ruby 395 | App = Rack::Builder.new do 396 | use Rack::Session::Cookie, secret: "..." 397 | 398 | run Syro.new { 399 | get do 400 | res.write("Hello, world") 401 | end 402 | } 403 | end 404 | ``` 405 | 406 | Trivia 407 | ------ 408 | 409 | An initial idea was to release a new version of [Cuba](http://cuba.is) 410 | that broke backward compatibility, but in the end my friends suggested 411 | to release this as a separate library. In the future, some ideas 412 | of this library could be included in Cuba as well. 413 | 414 | Installation 415 | ------------ 416 | 417 | ``` 418 | $ gem install syro 419 | ``` 420 | -------------------------------------------------------------------------------- /lib/syro.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # 3 | # Copyright (c) 2015 Michel Martens 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require "rack" 24 | require "seg" 25 | 26 | class Syro 27 | INBOX = "syro.inbox".freeze # :nodoc: 28 | 29 | class Response 30 | LOCATION = "Location".freeze # :nodoc: 31 | 32 | module ContentType 33 | HTML = "text/html".freeze # :nodoc: 34 | TEXT = "text/plain".freeze # :nodoc: 35 | JSON = "application/json".freeze # :nodoc: 36 | end 37 | 38 | # The status of the response. 39 | # 40 | # res.status = 200 41 | # res.status # => 200 42 | # 43 | attr_accessor :status 44 | 45 | # Returns the body of the response. 46 | # 47 | # res.body 48 | # # => [] 49 | # 50 | # res.write("there is") 51 | # res.write("no try") 52 | # 53 | # res.body 54 | # # => ["there is", "no try"] 55 | # 56 | attr_reader :body 57 | 58 | # Returns a hash with the response headers. 59 | # 60 | # res.headers 61 | # # => { "Content-Type" => "text/html", "Content-Length" => "42" } 62 | # 63 | attr_reader :headers 64 | 65 | def initialize(headers = {}) 66 | @status = 404 67 | @headers = headers 68 | @body = [] 69 | @length = 0 70 | end 71 | 72 | # Returns the response header corresponding to `key`. 73 | # 74 | # res["Content-Type"] # => "text/html" 75 | # res["Content-Length"] # => "42" 76 | # 77 | def [](key) 78 | @headers[key] 79 | end 80 | 81 | # Sets the given `value` with the header corresponding to `key`. 82 | # 83 | # res["Content-Type"] = "application/json" 84 | # res["Content-Type"] # => "application/json" 85 | # 86 | def []=(key, value) 87 | @headers[key] = value 88 | end 89 | 90 | # Appends `str` to `body` and updates the `Content-Length` header. 91 | # 92 | # res.body # => [] 93 | # 94 | # res.write("foo") 95 | # res.write("bar") 96 | # 97 | # res.body 98 | # # => ["foo", "bar"] 99 | # 100 | # res["Content-Length"] 101 | # # => 6 102 | # 103 | def write(str) 104 | s = str.to_s 105 | 106 | @length += s.bytesize 107 | @headers[Rack::CONTENT_LENGTH] = @length.to_s 108 | @body << s 109 | end 110 | 111 | # Write response body as text/plain 112 | def text(str) 113 | @headers[Rack::CONTENT_TYPE] = ContentType::TEXT 114 | write(str) 115 | end 116 | 117 | # Write response body as text/html 118 | def html(str) 119 | @headers[Rack::CONTENT_TYPE] = ContentType::HTML 120 | write(str) 121 | end 122 | 123 | # Write response body as application/json 124 | def json(str) 125 | @headers[Rack::CONTENT_TYPE] = ContentType::JSON 126 | write(str) 127 | end 128 | 129 | # Sets the `Location` header to `path` and updates the status to 130 | # `status`. By default, `status` is `302`. 131 | # 132 | # res.redirect("/path") 133 | # 134 | # res["Location"] # => "/path" 135 | # res.status # => 302 136 | # 137 | # res.redirect("http://syro.ru", 303) 138 | # 139 | # res["Location"] # => "http://syro.ru" 140 | # res.status # => 303 141 | # 142 | def redirect(path, status = 302) 143 | @headers[LOCATION] = path 144 | @status = status 145 | end 146 | 147 | # Returns an array with three elements: the status, headers and 148 | # body. If the status is not set, the status is set to 404. If 149 | # a match is found for both path and request method, the status 150 | # is changed to 200. 151 | # 152 | # res.status = 200 153 | # res.finish 154 | # # => [200, {}, []] 155 | # 156 | # res.status = nil 157 | # res.finish 158 | # # => [404, {}, []] 159 | # 160 | # res.status = nil 161 | # res.write("syro") 162 | # res.finish 163 | # # => [200, { "Content-Type" => "text/html" }, ["syro"]] 164 | # 165 | def finish 166 | [@status, @headers, @body] 167 | end 168 | 169 | # Sets a cookie into the response. 170 | # 171 | # res.set_cookie("foo", "bar") 172 | # res["Set-Cookie"] # => "foo=bar" 173 | # 174 | # res.set_cookie("foo2", "bar2") 175 | # res["Set-Cookie"] # => "foo=bar\nfoo2=bar2" 176 | # 177 | # res.set_cookie("bar", { 178 | # domain: ".example.com", 179 | # path: "/", 180 | # # max_age: 0, 181 | # # expires: Time.now + 10_000, 182 | # secure: true, 183 | # httponly: true, 184 | # value: "bar" 185 | # }) 186 | # 187 | # res["Set-Cookie"].split("\n").last 188 | # # => "bar=bar; domain=.example.com; path=/; secure; HttpOnly 189 | # 190 | # **NOTE:** This method doesn't sign and/or encrypt the value of the cookie. 191 | # 192 | def set_cookie(key, value) 193 | Rack::Utils.set_cookie_header!(@headers, key, value) 194 | end 195 | 196 | # Deletes given cookie. 197 | # 198 | # res.set_cookie("foo", "bar") 199 | # res["Set-Cookie"] 200 | # # => "foo=bar" 201 | # 202 | # res.delete_cookie("foo") 203 | # res["Set-Cookie"] 204 | # # => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" 205 | # 206 | def delete_cookie(key, value = {}) 207 | Rack::Utils.delete_cookie_header!(@headers, key, value) 208 | end 209 | end 210 | 211 | class Deck 212 | 213 | # Attaches the supplied block to a subclass of Deck as #dispatch! 214 | # Returns the subclassed Deck. 215 | def self.implement(&code) 216 | Class.new(self) do 217 | define_method(:dispatch!, code) 218 | private :dispatch! 219 | 220 | # Instead of calling inspect on this anonymous class, 221 | # defer to the superclass which is likely Syro::Deck. 222 | define_method(:inspect) do 223 | self.class.superclass.inspect 224 | end 225 | end 226 | end 227 | 228 | module API 229 | def env 230 | @syro_env 231 | end 232 | 233 | # Returns the incoming request object. This object is an 234 | # instance of Rack::Request. 235 | # 236 | # req.post? # => true 237 | # req.params # => { "username" => "bob", "password" => "secret" } 238 | # req[:username] # => "bob" 239 | # 240 | def req 241 | @syro_req 242 | end 243 | 244 | # Returns the current response object. This object is an 245 | # instance of Syro::Response. 246 | # 247 | # res.status = 200 248 | # res["Content-Type"] = "text/html" 249 | # res.write("