syro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []