rosendo 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.
- data/LICENSE +16 -0
- data/README.md +87 -0
- data/example.rb +43 -0
- data/lib/rosendo.rb +8 -0
- data/lib/rosendo/app.rb +35 -0
- data/lib/rosendo/dsl.rb +24 -0
- data/lib/rosendo/request.rb +27 -0
- data/lib/rosendo/response.rb +34 -0
- data/lib/rosendo/routes.rb +86 -0
- data/lib/rosendo/server.rb +37 -0
- data/test/integration/basic_test.rb +134 -0
- data/test/test_helper.rb +60 -0
- data/test/unit/sample_test.rb +7 -0
- metadata +79 -0
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Copyright (c) 2013 Sergio Gil
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
4
|
+
associated documentation files (the "Software"), to deal in the Software without restriction,
|
5
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
6
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
7
|
+
furnished to do so, subject to the following conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
10
|
+
portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
13
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
14
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
15
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
16
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Rosendo [](https://travis-ci.org/porras/rosendo)
|
2
|
+
|
3
|
+
Rosendo is a minimalistic and naive [Sinatra](http://sinatrarb.com) reimplementation, without any
|
4
|
+
dependencies other than the ruby socket library. It's a learning exercise on the HTTP specs, web
|
5
|
+
servers, and how not to write software. It contains a (stupidly simple) HTTP server, a (rather
|
6
|
+
incomplete) HTTP parser and a (really oversimplified) DSL.
|
7
|
+
|
8
|
+
**Rosendo is not intended for any production use. Specifically, *it's not intended as a Sinatra
|
9
|
+
replacement*. It has much less features, for sure much more bugs, and probably much worse
|
10
|
+
performance. It's just a learning exercise.**
|
11
|
+
|
12
|
+
This ongoing exercise is a game, whose rules (invented by myself) consist in reimplementing a web
|
13
|
+
framework (Sinatra is far better for this purpose than my other favorite framework Rails, for
|
14
|
+
obvious reasons) from scratch, using just Ruby. I wanted to prevent myself from using anything from
|
15
|
+
the standard library, but probably making this without the socket library would be too much, so
|
16
|
+
that's the only allowed exception. Still, no webrick, no erb, no nothing, and obviously no gems.
|
17
|
+
|
18
|
+
The rule doesn't apply to the tests though (I'm using minitest and Net::HTTP).
|
19
|
+
|
20
|
+
The purpose of the game/exercise comes from a reflection about how much software we usually depend
|
21
|
+
on when writing our applications (just run `bundle show` in your last Rails app if you don't believe
|
22
|
+
me) and is double:
|
23
|
+
|
24
|
+
1) Remember that the software we take for granted and like to whine about some times, it's actually
|
25
|
+
great software we should thank for every minute we're using it. Trying to live without it for some
|
26
|
+
time is a great way to achieve this.
|
27
|
+
|
28
|
+
2) At the same time, demystifying it. Not everybody can write such good web servers like the ones we
|
29
|
+
use everyday, but everybody can write the simplest one. And understanding a bit how they work is a
|
30
|
+
great thing, even if you're not obviously going to write a web server every time you want to write a
|
31
|
+
web app. The same is true for every other piece of the stack.
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
Install it via Rubygems or Bundler, require it, and pretend it's Sinatra:
|
36
|
+
|
37
|
+
require 'rosendo'
|
38
|
+
|
39
|
+
class MyApp < Rosendo::App
|
40
|
+
get "/" do
|
41
|
+
"Hello, World!"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
MyApp.run!(port: 2000)
|
46
|
+
|
47
|
+
## Features
|
48
|
+
|
49
|
+
### Sinatra features that Rosendo supports
|
50
|
+
|
51
|
+
* Route mapping methods (`get`, `post`, ...)
|
52
|
+
* Headers reading and setting
|
53
|
+
* Basic params in URL (`/hello/:name`)
|
54
|
+
* HTTP status code
|
55
|
+
* Redirects
|
56
|
+
* Params in query string
|
57
|
+
|
58
|
+
### Sinatra features that Rosendo plans to support
|
59
|
+
|
60
|
+
* Form params in request body
|
61
|
+
* Templates
|
62
|
+
|
63
|
+
### Sinatra features that Rosendo doesn't plan to support (for the moment)
|
64
|
+
|
65
|
+
* Advanced route patterns (*, regular expressions, conditions, ...)
|
66
|
+
* Filters
|
67
|
+
* Helpers
|
68
|
+
* Variables (`set` method)
|
69
|
+
* Cookies
|
70
|
+
* Sessions
|
71
|
+
* Config blocks
|
72
|
+
* Rack compliance
|
73
|
+
* Streaming
|
74
|
+
* Logging
|
75
|
+
* *Classic* mode
|
76
|
+
* Static files
|
77
|
+
|
78
|
+
See [`example.rb`](https://github.com/porras/rosendo/blob/master/example.rb) for some supported things.
|
79
|
+
|
80
|
+
[Rosendo](http://en.wikipedia.org/wiki/Rosendo_Mercado) is also the name of the most charismatic
|
81
|
+
Spanish rock singer and songwriter ever.
|
82
|
+
|
83
|
+
[](http://en.wikipedia.org/wiki/Rosendo_Mercado)
|
84
|
+
|
85
|
+
## License
|
86
|
+
|
87
|
+
Released under the [MIT license](https://github.com/porras/rosendo/blob/master/LICENSE).
|
data/example.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require './lib/rosendo'
|
2
|
+
|
3
|
+
class Example < Rosendo::App
|
4
|
+
get '/' do
|
5
|
+
'Hola mundo'
|
6
|
+
end
|
7
|
+
|
8
|
+
get '/wadus' do
|
9
|
+
'Hola wadus'
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/hello/:name/:surname' do
|
13
|
+
"#{params[:surname]}, #{params[:name]}"
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/berlin' do
|
17
|
+
'Hola Berlin'
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/headers' do
|
21
|
+
headers 'X-Wadus' => 'Wadus!!'
|
22
|
+
"Received headers: #{request.env.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
get '/status/:code' do
|
26
|
+
status params[:code].to_i
|
27
|
+
"#{params[:code]} Invented Status"
|
28
|
+
end
|
29
|
+
|
30
|
+
get '/redirect' do
|
31
|
+
redirect '/'
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/params' do
|
35
|
+
params.inspect
|
36
|
+
end
|
37
|
+
|
38
|
+
get '/exception' do
|
39
|
+
raise 'Catacrocker'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Example.run!(port: 2000)
|
data/lib/rosendo.rb
ADDED
data/lib/rosendo/app.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rosendo
|
2
|
+
class App
|
3
|
+
class << self
|
4
|
+
%w{GET POST PUT DELETE}.each do |method|
|
5
|
+
define_method method.downcase do |*args, &block|
|
6
|
+
routes.add(method, *args, &block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def run!(options = {})
|
11
|
+
Server.new(self, options).start
|
12
|
+
end
|
13
|
+
|
14
|
+
def process(request, response)
|
15
|
+
if route = routes.for(request)
|
16
|
+
response.body = begin
|
17
|
+
route.call(request, response)
|
18
|
+
rescue Exception => e
|
19
|
+
response.status = 500
|
20
|
+
e.inspect + "\n\n" + e.backtrace.join("\n")
|
21
|
+
end
|
22
|
+
else
|
23
|
+
response.status = 404
|
24
|
+
response.body = "404 Not Found"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def routes
|
31
|
+
@routes ||= Routes.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rosendo/dsl.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rosendo
|
2
|
+
class DSL
|
3
|
+
attr_reader :request, :params
|
4
|
+
def initialize(request, response, params)
|
5
|
+
@request = request
|
6
|
+
@response = response
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
def headers(extra = {})
|
11
|
+
@response.headers.merge!(extra)
|
12
|
+
end
|
13
|
+
|
14
|
+
def status(code)
|
15
|
+
@response.status = code
|
16
|
+
end
|
17
|
+
|
18
|
+
def redirect(url, code = 302, content = "")
|
19
|
+
status code
|
20
|
+
headers 'Location' => url
|
21
|
+
content
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rosendo
|
2
|
+
class Request
|
3
|
+
attr_reader :method, :url, :headers
|
4
|
+
alias_method :env, :headers
|
5
|
+
def initialize(io)
|
6
|
+
@io = io
|
7
|
+
@method, @url = @io.gets.match(%r{(GET|POST|PUT|DELETE)\s(.+)\sHTTP/1\.1})[1..2]
|
8
|
+
@headers = read_headers
|
9
|
+
end
|
10
|
+
|
11
|
+
def params
|
12
|
+
{}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def read_headers
|
18
|
+
{}.tap do |h|
|
19
|
+
while line = @io.gets.chomp
|
20
|
+
break if line.empty?
|
21
|
+
m = line.match(%r{^([\w\-]+):\s(.+)$})
|
22
|
+
h[m[1]] = m[2]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rosendo
|
2
|
+
class Response
|
3
|
+
attr_accessor :status, :headers, :body
|
4
|
+
def initialize(io)
|
5
|
+
@io = io
|
6
|
+
@headers = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def status
|
10
|
+
@status || 200
|
11
|
+
end
|
12
|
+
|
13
|
+
def body
|
14
|
+
@body || ''
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond
|
18
|
+
@io.puts status_line
|
19
|
+
@io.puts header_lines
|
20
|
+
@io.puts
|
21
|
+
@io.write body # write instead of puts for no extra newline
|
22
|
+
@io.close
|
23
|
+
end
|
24
|
+
|
25
|
+
def status_line
|
26
|
+
"HTTP/1.1 #{status}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def header_lines
|
30
|
+
{'Content-Length' => body.size}.merge(@headers).map { |k, v| "#{k}: #{v}" }.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Rosendo
|
2
|
+
class Routes
|
3
|
+
def initialize
|
4
|
+
@routes = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def <<(route)
|
8
|
+
@routes << route
|
9
|
+
end
|
10
|
+
|
11
|
+
def for(request)
|
12
|
+
@routes.detect { |route| route.matches?(request) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(method, path, &block)
|
16
|
+
self << Route.new(method, path, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
class Route
|
20
|
+
attr_reader :method, :path
|
21
|
+
def initialize(method, path, &block)
|
22
|
+
@method = method
|
23
|
+
@path = Path.new(path)
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def matches?(request)
|
28
|
+
method == request.method &&
|
29
|
+
path.matches?(request.url)
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(request, response)
|
33
|
+
DSL.new(request, response, path.params(request.url)).instance_eval(&@block)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Path
|
37
|
+
def initialize(path)
|
38
|
+
@path = path
|
39
|
+
@regexp, @keys = parse
|
40
|
+
end
|
41
|
+
|
42
|
+
def matches?(url)
|
43
|
+
url = URL.new(url)
|
44
|
+
url.path =~ @regexp
|
45
|
+
end
|
46
|
+
|
47
|
+
def params(url)
|
48
|
+
url = URL.new(url)
|
49
|
+
match = url.path.match(@regexp)
|
50
|
+
{}.tap do |params|
|
51
|
+
@keys.each_with_index do |key, i|
|
52
|
+
params[key] = match[i + 1]
|
53
|
+
end
|
54
|
+
end.merge(url.query_params)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse
|
58
|
+
keys = []
|
59
|
+
pattern = @path.gsub(/:(\w+)/) do |match|
|
60
|
+
keys << $1.to_sym
|
61
|
+
'(\w+)'
|
62
|
+
end
|
63
|
+
[Regexp.new("^#{pattern}$"), keys]
|
64
|
+
end
|
65
|
+
|
66
|
+
class URL
|
67
|
+
attr_reader :url, :path, :query
|
68
|
+
def initialize(url)
|
69
|
+
@url = url
|
70
|
+
@path, @query = url.split('?')
|
71
|
+
end
|
72
|
+
|
73
|
+
def query_params
|
74
|
+
return {} unless query
|
75
|
+
{}.tap do |params|
|
76
|
+
query.split('&').each do |pair|
|
77
|
+
k, v = pair.split('=')
|
78
|
+
params[k.to_sym] = v
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Rosendo
|
4
|
+
class Server
|
5
|
+
class Stop < Exception; end
|
6
|
+
|
7
|
+
attr_reader :port, :out
|
8
|
+
def initialize(app, args = {})
|
9
|
+
@app = app
|
10
|
+
@port = args[:port] || '2000'
|
11
|
+
@out = args[:out] || STDOUT
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
out.puts "== Rosendo is rocking the stage on #{port}"
|
16
|
+
out.puts ">> Listening on 0.0.0.0:#{port}, CTRL+C to stop"
|
17
|
+
|
18
|
+
loop do
|
19
|
+
begin
|
20
|
+
client = server.accept
|
21
|
+
request = Request.new(client)
|
22
|
+
response = Response.new(client)
|
23
|
+
@app.process(request, response)
|
24
|
+
response.respond
|
25
|
+
out.puts "#{request.method} #{request.url} #{response.status} #{response.body.size}"
|
26
|
+
rescue Stop
|
27
|
+
server.close
|
28
|
+
out.puts "== Rosendo has left the building (everybody goes crazy)"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def server
|
34
|
+
@server ||= TCPServer.new(@port)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'rosendo'
|
3
|
+
|
4
|
+
class BasicTest < IntegrationTest
|
5
|
+
def test_hello_world
|
6
|
+
app do
|
7
|
+
get('/') { 'Hello, World!' }
|
8
|
+
end
|
9
|
+
|
10
|
+
get '/'
|
11
|
+
|
12
|
+
assert_equal(200, response.status)
|
13
|
+
assert_equal('Hello, World!', response.body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_simple_route
|
17
|
+
app do
|
18
|
+
get('/wadus') { 'Wadus' }
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/wadus'
|
22
|
+
|
23
|
+
assert_equal(200, response.status)
|
24
|
+
assert_equal('Wadus', response.body)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_not_found
|
28
|
+
app do
|
29
|
+
get('/') { 'Hello' }
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/foo'
|
33
|
+
|
34
|
+
assert_equal(404, response.status)
|
35
|
+
assert_equal('404 Not Found', response.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_content_size
|
39
|
+
app do
|
40
|
+
get('/') { 'a' * rand(1000) }
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/'
|
44
|
+
|
45
|
+
assert_equal(response.body.size, response.headers['content-length'][0].to_i)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_params_in_url
|
49
|
+
app do
|
50
|
+
get('/hello/:name/:surname') { "Hola, #{params[:name].capitalize} #{params[:surname].capitalize}"}
|
51
|
+
end
|
52
|
+
|
53
|
+
get '/hello/rosendo/mercado'
|
54
|
+
|
55
|
+
assert_equal(200, response.status)
|
56
|
+
assert_equal('Hola, Rosendo Mercado', response.body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_read_headers
|
60
|
+
app do
|
61
|
+
get('/headers') { request.env['X-Wadus'] }
|
62
|
+
end
|
63
|
+
|
64
|
+
get '/headers', 'X-Wadus' => 'Wadus'
|
65
|
+
|
66
|
+
assert_equal(200, response.status)
|
67
|
+
assert_equal('Wadus', response.body)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_set_headers
|
71
|
+
app do
|
72
|
+
get('/headers') do
|
73
|
+
headers('X-Wadus' => 'Wadus')
|
74
|
+
'Header set'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
get '/headers'
|
79
|
+
|
80
|
+
assert_equal(200, response.status)
|
81
|
+
assert_equal('Header set', response.body)
|
82
|
+
assert_equal(['Wadus'], response.headers['x-wadus']) # header names are case insensitive per RFC,
|
83
|
+
# and Net::HTTP seems to implement this simply
|
84
|
+
# downcasing them. Also, it returns a list
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_status_code
|
88
|
+
app do
|
89
|
+
get('/status/:code') do
|
90
|
+
status params[:code].to_i
|
91
|
+
"#{params[:code]} Invented Status"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
get '/status/234'
|
96
|
+
|
97
|
+
assert_equal(234, response.status)
|
98
|
+
assert_equal('234 Invented Status', response.body)
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_redirect
|
102
|
+
app do
|
103
|
+
get('/') { redirect '/home' }
|
104
|
+
end
|
105
|
+
|
106
|
+
get '/'
|
107
|
+
|
108
|
+
assert_equal(302, response.status)
|
109
|
+
assert_equal(['/home'], response.headers['location'])
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_params
|
113
|
+
app do
|
114
|
+
get('/params') { "a: #{params[:a]}; b: #{params[:b]}" }
|
115
|
+
end
|
116
|
+
|
117
|
+
get '/params?a=AAA&b=BBB'
|
118
|
+
|
119
|
+
assert_equal(200, response.status)
|
120
|
+
assert_equal("a: AAA; b: BBB", response.body)
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_exception
|
124
|
+
app do
|
125
|
+
get('/exception') { raise 'Catacrocker' }
|
126
|
+
end
|
127
|
+
|
128
|
+
get '/exception'
|
129
|
+
|
130
|
+
assert_equal(500, response.status)
|
131
|
+
assert_match('Catacrocker', response.body)
|
132
|
+
assert_match(__FILE__, response.body)
|
133
|
+
end
|
134
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'minitest/unit'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module TestHelper
|
7
|
+
module IntegrationHelper
|
8
|
+
PORT = 2600
|
9
|
+
BASE_URL = "http://localhost:#{PORT}"
|
10
|
+
|
11
|
+
def app(&block)
|
12
|
+
Thread.current[:app] = Thread.new do
|
13
|
+
Class.new(Rosendo::App, &block).run!(port: PORT, out: File.open(File::NULL, "w"))
|
14
|
+
end
|
15
|
+
sleep (ENV['WAIT'] && ENV['WAIT'].to_f) || 0.001
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(path, headers = {})
|
19
|
+
uri = URI.parse(BASE_URL + path)
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
21
|
+
response = http.get(path, headers)
|
22
|
+
@last_response = Response.new(response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def response
|
26
|
+
@last_response
|
27
|
+
end
|
28
|
+
|
29
|
+
class Response
|
30
|
+
def initialize(response)
|
31
|
+
@response = response
|
32
|
+
end
|
33
|
+
|
34
|
+
def body
|
35
|
+
@response.body
|
36
|
+
end
|
37
|
+
|
38
|
+
def status
|
39
|
+
@response.code.to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
def headers
|
43
|
+
@response.to_hash
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class UnitTest < MiniTest::Unit::TestCase
|
50
|
+
include TestHelper
|
51
|
+
end
|
52
|
+
|
53
|
+
class IntegrationTest < UnitTest
|
54
|
+
include IntegrationHelper
|
55
|
+
|
56
|
+
def teardown
|
57
|
+
return unless app = Thread.current[:app]
|
58
|
+
app.raise Rosendo::Server::Stop
|
59
|
+
end
|
60
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rosendo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sergio Gil
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-03-24 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: sgilperez@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README.md
|
29
|
+
files:
|
30
|
+
- README.md
|
31
|
+
- LICENSE
|
32
|
+
- example.rb
|
33
|
+
- lib/rosendo/app.rb
|
34
|
+
- lib/rosendo/dsl.rb
|
35
|
+
- lib/rosendo/request.rb
|
36
|
+
- lib/rosendo/response.rb
|
37
|
+
- lib/rosendo/routes.rb
|
38
|
+
- lib/rosendo/server.rb
|
39
|
+
- lib/rosendo.rb
|
40
|
+
- test/integration/basic_test.rb
|
41
|
+
- test/test_helper.rb
|
42
|
+
- test/unit/sample_test.rb
|
43
|
+
homepage: http://github.com/porras/rosendo
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --main
|
49
|
+
- README.md
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 3
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.8.24
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Minimalistic and naive Sinatra reimplementation, without any dependencies other than the ruby socket library
|
77
|
+
test_files: []
|
78
|
+
|
79
|
+
has_rdoc:
|