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 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: