rosendo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -0,0 +1,87 @@
1
+ # Rosendo [![Build Status](https://travis-ci.org/porras/rosendo.png)](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
+ [![Rosendo Mercado](http://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Rosendo_-_11.jpg/320px-Rosendo_-_11.jpg)](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).
@@ -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)
@@ -0,0 +1,8 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rosendo/server'
4
+ require 'rosendo/request'
5
+ require 'rosendo/response'
6
+ require 'rosendo/app'
7
+ require 'rosendo/routes'
8
+ require 'rosendo/dsl'
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class SampleTest < UnitTest
4
+ def test_truth
5
+ assert true
6
+ end
7
+ 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: