syro 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c674acd1e5f50201c1a6d8ba682f73376922bf06
4
+ data.tar.gz: cf8130872298e53a52b0da3358c8c7929b81ce4b
5
+ SHA512:
6
+ metadata.gz: 8fb83225dc60423a43c56147159e679be6349a5da972ba9183b7bcbfe12091c630046c8e0f28d9570609f1bf64e082802c7aac7c42ac56c4025dd796cd0b08b9
7
+ data.tar.gz: a9ed177ed81d563450db71b2b30053ee2bf8c11f40dc1df3778de92de71ec7b3606895cc90413aba84af78bdb435c00fbee85483aecabefb2674bb94aec3dc01
data/.gems ADDED
@@ -0,0 +1,4 @@
1
+ rack -v 1.6.0
2
+ rack-test -v 0.6.3
3
+ cutest -v 1.2.2
4
+ seg -v 0.0.1
File without changes
@@ -0,0 +1,19 @@
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.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
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.
@@ -0,0 +1,211 @@
1
+ Syro
2
+ ====
3
+
4
+ Simple router for web applications.
5
+
6
+ Description
7
+ -----------
8
+
9
+ Syro is a very simple router for web applications. It was created
10
+ in the tradition of libraries like [Rum][rum] and [Cuba][cuba], but
11
+ it promotes a less flexible usage pattern. The design is inspired
12
+ by the way some Cuba applications are architected: modularity is
13
+ encouraged and sub-applications can be dispatched without any
14
+ significant performance overhead.
15
+
16
+ [rum]: http://github.com/chneukirchen/rum
17
+ [cuba]: http://cuba.is
18
+
19
+ Usage
20
+ -----
21
+
22
+ An example of a modular application would look like this:
23
+
24
+ ```ruby
25
+ admin = Syro.new {
26
+ get {
27
+ res.write "Hello from admin!"
28
+ }
29
+ }
30
+
31
+ app = Syro.new {
32
+ on("admin") {
33
+ run(admin)
34
+ }
35
+ }
36
+ ```
37
+
38
+ The block is evaluated in a sandbox where the following methods are
39
+ available: `env`, `req`, `res`, `inbox`, `call`, `run`, `halt`,
40
+ `match`, `on`, `root?`, `root`, `get?`, `post?`, `patch?`, `delete?`,
41
+ `get`, `post`, `patch` and `delete`.
42
+
43
+ As a recommendation, user created variables should be instance
44
+ variables. That way they won't mix with the API methods defined in
45
+ the sandbox. All the internal instance variables defined by Syro
46
+ are prefixed by `syro_`, like in `@syro_inbox`.
47
+
48
+ API
49
+ ---
50
+
51
+ `env`: Environment variables for the request.
52
+
53
+ `req`: Helper object for accessing the request variables. It's an
54
+ instance of `Rack::Request`.
55
+
56
+ `res`: Helper object for creating the response. It's an instance
57
+ of `Syro::Response`.
58
+
59
+ `inbox`: Hash with captures and potentially other variables local
60
+ to the request.
61
+
62
+ `call`: Entry point for the application. It receives the environment
63
+ and optionally an inbox.
64
+
65
+ `run`: Runs a sub app, and accepts an inbox as an optional second
66
+ argument.
67
+
68
+ `halt`: Terminates the request. It receives an array with the
69
+ response as per Rack's specification.
70
+
71
+ `match`: Receives a String, a Symbol or a boolean, and returns true
72
+ if it matches the request.
73
+
74
+ `on`: Receives a value to be matched, and a block that will be
75
+ executed only if the request is matched.
76
+
77
+ `root?`: Returns true if the path yet to be consumed is empty.
78
+
79
+ `root`: Receives a block and calls it only if `root?` is true.
80
+
81
+ `get?`: Returns true if the `REQUEST_METHOD` is `GET`.
82
+
83
+ `get`: Receives a block and calls it only if `root?` and `get?` are
84
+ true.
85
+
86
+ `post?`: Returns true if the `REQUEST_METHOD` is `POST`.
87
+
88
+ `post`: Receives a block and calls it only if `root?` and `post?`
89
+ are true.
90
+
91
+ `patch?`: Returns true if the `REQUEST_METHOD` is `PATCH`.
92
+
93
+ `patch`: Receives a block and calls it only if `root?` and `patch?`
94
+ are true.
95
+
96
+ `delete?`: Returns true if the `REQUEST_METHOD` is `DELETE`.
97
+
98
+ `delete`: Receives a block and calls it only if `root?` and `delete?`
99
+ are true.
100
+
101
+ Examples
102
+ --------
103
+
104
+ In the following examples, the response string represents
105
+ the request path that was sent.
106
+
107
+ ```ruby
108
+ app = Syro.new {
109
+ get {
110
+ res.write "GET /"
111
+ }
112
+
113
+ post {
114
+ res.write "POST /"
115
+ }
116
+
117
+ on("users") {
118
+ on(:id) {
119
+
120
+ # Captured values go to the inbox
121
+ @user = User[inbox[:id]]
122
+
123
+ get {
124
+ res.write "GET /users/42"
125
+ }
126
+
127
+ patch {
128
+ res.write "PATCH /users/42"
129
+ }
130
+
131
+ delete {
132
+ res.write "DELETE /users/42"
133
+ }
134
+ }
135
+
136
+ get {
137
+ res.write "GET /users"
138
+ }
139
+
140
+ post {
141
+ res.write "POST /users"
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Matches
148
+ -------
149
+
150
+ The `on` method can receive a `String` to perform path matches; a
151
+ `Symbol` to perform path captures; and a boolean to match any true
152
+ values.
153
+
154
+ Each time `on` matches or captures a segment of the PATH, that part
155
+ of the path is consumed. The current and previous paths can be
156
+ queried by calling `prev` and `curr` on the `path` object: `path.prev`
157
+ returns the part of the path already consumed, and `path.curr`
158
+ provides the current version of the path.
159
+
160
+ Any expression that evaluates to a boolean can also be used as a
161
+ matcher. For example, a common pattern is to follow some route
162
+ only if a user is authenticated. That can be accomplished with
163
+ `on(authenticated(User))`. That example assumes there's a method
164
+ called `authenticated` that returns true or false depending on
165
+ whether or not an instance of `User` is authenticated. As a side
166
+ note, [Shield][shield] is a library that provides just that.
167
+
168
+ [shield]: https://github.com/cyx/shield
169
+
170
+ Captures
171
+ --------
172
+
173
+ When a symbol is provided, `on` will try to consume a segment of
174
+ the path. A segment is defined as any sequence of characters after
175
+ a slash and until either another slash or the end of the string.
176
+ The captured value is stored in the `inbox` hash under the key that
177
+ was provided as the argument to `on`. For example, after a call to
178
+ `on(:user_id)`, the value for the segment will be stored at
179
+ `inbox[:user_id]`. When mounting an application called `users` with
180
+ the command `run(users)`, an inbox can be provided as the second
181
+ argument: `run(users, inbox)`. That allows apps to share previous
182
+ captures.
183
+
184
+ Security
185
+ --------
186
+
187
+ There are no security features built into this routing library. A
188
+ framework using this library should implement the security layer.
189
+
190
+ Rendering
191
+ ---------
192
+
193
+ There are no rendering features built into this routing library. A
194
+ framework that uses this routing library can easily implement helpers
195
+ for rendering.
196
+
197
+ Trivia
198
+ ------
199
+
200
+ The name comes from SImple ROuter, so it more or less rhymes with
201
+ "zero". An initial idea was to release a new version of Cuba that
202
+ broke backward compatibility, but in the end my friends suggested
203
+ to release this as a separate library. In the future, some ideas
204
+ of this library could be included in Cuba as well.
205
+
206
+ Installation
207
+ ------------
208
+
209
+ ```
210
+ $ gem install syro
211
+ ```
@@ -0,0 +1,257 @@
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
+
28
+ # Method override parameter
29
+ OVERRIDE = "_method".freeze
30
+
31
+ # HTTP environment variables
32
+ PATH_INFO = "PATH_INFO".freeze
33
+ SCRIPT_NAME = "SCRIPT_NAME".freeze
34
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
35
+
36
+ # Response headers
37
+ LOCATION = "Location".freeze
38
+ CONTENT_TYPE = "Content-Type".freeze
39
+ CONTENT_LENGTH = "Content-Length".freeze
40
+ CONTENT_TYPE_DEFAULT = "text/html".freeze
41
+
42
+ # Request methods
43
+ GET = 'GET'.freeze
44
+ POST = 'POST'.freeze
45
+ PATCH = 'PATCH'.freeze
46
+ DELETE = 'DELETE'.freeze
47
+
48
+ # Available methods
49
+ METHODS = [GET, POST, PATCH, DELETE].freeze
50
+
51
+ class Response
52
+ attr_accessor :status
53
+
54
+ attr :body
55
+ attr :headers
56
+
57
+ def initialize(headers = {})
58
+ @status = nil
59
+ @headers = headers
60
+ @body = []
61
+ @length = 0
62
+ end
63
+
64
+ def [](key)
65
+ @headers[key]
66
+ end
67
+
68
+ def []=(key, value)
69
+ @headers[key] = value
70
+ end
71
+
72
+ def write(str)
73
+ s = str.to_s
74
+
75
+ @length += s.bytesize
76
+ @headers[Syro::CONTENT_LENGTH] = @length.to_s
77
+ @body << s
78
+ end
79
+
80
+ def redirect(path, status = 302)
81
+ @headers[Syro::LOCATION] = path
82
+ @status = status
83
+ end
84
+
85
+ def finish
86
+ [@status, @headers, @body]
87
+ end
88
+
89
+ def set_cookie(key, value)
90
+ Rack::Utils.set_cookie_header!(@headers, key, value)
91
+ end
92
+
93
+ def delete_cookie(key, value = {})
94
+ Rack::Utils.delete_cookie_header!(@headers, key, value)
95
+ end
96
+ end
97
+
98
+ class Sandbox
99
+ def initialize(code)
100
+ @syro_code = code
101
+ end
102
+
103
+ def env
104
+ @syro_env
105
+ end
106
+
107
+ def req
108
+ @syro_req
109
+ end
110
+
111
+ def res
112
+ @syro_res
113
+ end
114
+
115
+ def path
116
+ @syro_path
117
+ end
118
+
119
+ def inbox
120
+ @syro_inbox
121
+ end
122
+
123
+ def call(env, inbox)
124
+ @syro_env = env
125
+ @syro_req = Rack::Request.new(env)
126
+ @syro_res = Syro::Response.new({})
127
+ @syro_path = Seg.new(env.fetch(Syro::PATH_INFO))
128
+ @syro_inbox = inbox
129
+
130
+
131
+ if env[Syro::REQUEST_METHOD] == Syro::POST
132
+ value = @syro_req.POST[Syro::OVERRIDE]
133
+
134
+ if value != nil
135
+ env[Syro::REQUEST_METHOD] = value.upcase
136
+ end
137
+ end
138
+
139
+ @syro_method = Syro::METHODS.index(env[Syro::REQUEST_METHOD])
140
+
141
+ result = catch(:halt) do
142
+ instance_eval(&@syro_code)
143
+
144
+ @syro_res.status = 404
145
+ @syro_res.finish
146
+ end
147
+
148
+ if result[0].nil?
149
+ if result[2].empty?
150
+ result[0] = 404
151
+ else
152
+ result[1][Syro::CONTENT_TYPE] ||=
153
+ Syro::CONTENT_TYPE_DEFAULT
154
+ result[0] = 200
155
+ end
156
+ end
157
+
158
+ result
159
+ end
160
+
161
+ def run(app, inbox = {})
162
+ env[Syro::PATH_INFO] = @syro_path.curr
163
+ env[Syro::SCRIPT_NAME] = @syro_path.prev
164
+
165
+ halt(app.call(env, inbox))
166
+ end
167
+
168
+ def halt(response)
169
+ throw(:halt, response)
170
+ end
171
+
172
+ def match(arg)
173
+ case arg
174
+ when String then @syro_path.consume(arg)
175
+ when Symbol then @syro_path.capture(arg, inbox)
176
+ when true then true
177
+ else false
178
+ end
179
+ end
180
+
181
+ def on(arg)
182
+ if match(arg)
183
+ yield
184
+
185
+ halt(res.finish)
186
+ end
187
+ end
188
+
189
+ def root?
190
+ @syro_path.root?
191
+ end
192
+
193
+ def root
194
+ if root?
195
+ yield
196
+
197
+ halt(res.finish)
198
+ end
199
+ end
200
+
201
+ def get?
202
+ @syro_method == 0
203
+ end
204
+
205
+ def post?
206
+ @syro_method == 1
207
+ end
208
+
209
+ def patch?
210
+ @syro_method == 2
211
+ end
212
+
213
+ def delete?
214
+ @syro_method == 3
215
+ end
216
+
217
+ def get
218
+ if root? && get?
219
+ yield
220
+
221
+ halt(res.finish)
222
+ end
223
+ end
224
+
225
+ def post
226
+ if root? && post?
227
+ yield
228
+
229
+ halt(res.finish)
230
+ end
231
+ end
232
+
233
+ def patch
234
+ if root? && patch?
235
+ yield
236
+
237
+ halt(res.finish)
238
+ end
239
+ end
240
+
241
+ def delete
242
+ if root? && delete?
243
+ yield
244
+
245
+ halt(res.finish)
246
+ end
247
+ end
248
+ end
249
+
250
+ def initialize(&block)
251
+ @sandbox = Sandbox.new(block)
252
+ end
253
+
254
+ def call(env, inbox = {})
255
+ @sandbox.call(env, inbox)
256
+ end
257
+ end
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ cutest -r ./test/helper.rb ./test/*.rb
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "syro"
3
+ s.version = "0.0.1"
4
+ s.summary = "Simple router"
5
+ s.description = "Simple router for web applications"
6
+ s.authors = ["Michel Martens"]
7
+ s.email = ["michel@soveran.com"]
8
+ s.homepage = "https://github.com/soveran/syro"
9
+ s.license = "MIT"
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+
13
+ s.add_dependency "seg"
14
+ s.add_dependency "rack"
15
+ s.add_development_dependency "cutest"
16
+ s.add_development_dependency "rack-test"
17
+ end
@@ -0,0 +1,159 @@
1
+ admin = Syro.new {
2
+ get {
3
+ res.write "GET /admin"
4
+ }
5
+ }
6
+
7
+ platforms = Syro.new {
8
+ @id = inbox.fetch(:id)
9
+
10
+ get {
11
+ res.write "GET /platforms/#{@id}"
12
+ }
13
+ }
14
+
15
+ comments = Syro.new {
16
+ get {
17
+ res.write sprintf("GET %s/%s/comments",
18
+ inbox[:path],
19
+ inbox[:post_id])
20
+ }
21
+ }
22
+
23
+ app = Syro.new {
24
+ get {
25
+ res.write "GET /"
26
+ }
27
+
28
+ post {
29
+ on(req.POST["user"] != nil) {
30
+ res.write "POST / (user)"
31
+ }
32
+
33
+ on(true) {
34
+ res.write "POST / (none)"
35
+ }
36
+ }
37
+
38
+ on("foo") {
39
+ on("bar") {
40
+ on("baz") {
41
+ res.write("error")
42
+ }
43
+
44
+ get {
45
+ res.write("GET /foo/bar")
46
+ }
47
+
48
+ post {
49
+ res.write("POST /foo/bar")
50
+ }
51
+
52
+ patch {
53
+ res.write("PATCH /foo/bar")
54
+ }
55
+
56
+ delete {
57
+ res.write("DELETE /foo/bar")
58
+ }
59
+ }
60
+ }
61
+
62
+ on("bar/baz") {
63
+ get {
64
+ res.write("GET /bar/baz")
65
+ }
66
+ }
67
+
68
+ on("admin") {
69
+ run(admin)
70
+ }
71
+
72
+ on("platforms") {
73
+ run(platforms, id: 42)
74
+ }
75
+
76
+ on("users") {
77
+ on(:id) {
78
+ res.write(sprintf("GET /users/%s", inbox[:id]))
79
+ }
80
+ }
81
+
82
+ on("posts") {
83
+ @path = path.prev
84
+
85
+ on(:post_id) {
86
+ on("comments") {
87
+ run(comments, inbox.merge(path: @path))
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ setup do
94
+ Driver.new(app)
95
+ end
96
+
97
+ test "path + verb" do |f|
98
+ f.get("/foo/bar")
99
+ assert_equal 200, f.last_response.status
100
+ assert_equal "GET /foo/bar", f.last_response.body
101
+
102
+ f.patch("/foo/bar")
103
+ assert_equal 200, f.last_response.status
104
+ assert_equal "PATCH /foo/bar", f.last_response.body
105
+
106
+ f.post("/foo/bar")
107
+ assert_equal 200, f.last_response.status
108
+ assert_equal "POST /foo/bar", f.last_response.body
109
+
110
+ f.delete("/foo/bar")
111
+ assert_equal 200, f.last_response.status
112
+ assert_equal "DELETE /foo/bar", f.last_response.body
113
+ end
114
+
115
+ test "verbs match only on root" do |f|
116
+ f.get("/bar/baz/foo")
117
+ assert_equal "", f.last_response.body
118
+ assert_equal 404, f.last_response.status
119
+ end
120
+
121
+ test "mounted app" do |f|
122
+ f.get("/admin")
123
+ assert_equal "GET /admin", f.last_response.body
124
+ assert_equal 200, f.last_response.status
125
+ end
126
+
127
+ test "mounted app + inbox" do |f|
128
+ f.get("/platforms")
129
+ assert_equal "GET /platforms/42", f.last_response.body
130
+ assert_equal 200, f.last_response.status
131
+ end
132
+
133
+ test "root" do |f|
134
+ f.get("/")
135
+ assert_equal "GET /", f.last_response.body
136
+ assert_equal 200, f.last_response.status
137
+ end
138
+
139
+ test "captures" do |f|
140
+ f.get("/users/42")
141
+ assert_equal "GET /users/42", f.last_response.body
142
+ assert_equal 200, f.last_response.status
143
+ end
144
+
145
+ test "post values" do |f|
146
+ f.post("/", "user" => { "username" => "foo" })
147
+ assert_equal "POST / (user)", f.last_response.body
148
+ assert_equal 200, f.last_response.status
149
+
150
+ f.post("/")
151
+ assert_equal "POST / (none)", f.last_response.body
152
+ assert_equal 200, f.last_response.status
153
+ end
154
+
155
+ test "inherited inbox" do |f|
156
+ f.get("/posts/42/comments")
157
+ assert_equal "GET /posts/42/comments", f.last_response.body
158
+ assert_equal 200, f.last_response.status
159
+ end
@@ -0,0 +1,15 @@
1
+ require_relative "../lib/syro"
2
+ require "rack/test"
3
+
4
+ class Driver
5
+ include Rack::Test::Methods
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def app
12
+ @app
13
+ end
14
+ end
15
+
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: syro
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michel Martens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: seg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cutest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack-test
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Simple router for web applications
70
+ email:
71
+ - michel@soveran.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gems
77
+ - CHANGELOG
78
+ - CONTRIBUTING
79
+ - LICENSE
80
+ - README.md
81
+ - lib/syro.rb
82
+ - makefile
83
+ - syro.gemspec
84
+ - test/all.rb
85
+ - test/helper.rb
86
+ homepage: https://github.com/soveran/syro
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.0.14
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Simple router
110
+ test_files: []