angelo 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ angelo-*.gem
2
+ Gemfile.lock
data/CHANGELOG ADDED
@@ -0,0 +1,20 @@
1
+ changelog
2
+ =========
3
+
4
+ ### 0.0.3 30 oct 2013
5
+
6
+
7
+ * added tilt/erb
8
+ * added before/after
9
+ * added content_type/headers
10
+ * better websockets error handling
11
+
12
+ ### 0.0.2 29 oct 2013
13
+
14
+ * added websockets helper
15
+
16
+ ### 0.0.1 28 oct 2013
17
+
18
+ * initial release
19
+ * get/post/put/delete/options support
20
+ * socket support
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'reel'
4
+ gem 'pry'
5
+ gem 'pry-nav'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Kenichi Nakamura
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ Angelo
2
+ ======
3
+
4
+ A Sinatra-esque DSL for Reel.
5
+
6
+ __SUPER ALPHA!__
7
+
8
+ ### Quick example
9
+
10
+ ```ruby
11
+ require 'angelo'
12
+
13
+ class Foo < Angelo::Base
14
+
15
+ TEST = {foo: "bar", baz: 123, bat: false}.to_json
16
+
17
+ def pong; 'pong'; end
18
+ def foo; params[:foo]; end
19
+
20
+ get '/ping' do
21
+ pong
22
+ end
23
+
24
+ post '/foo' do
25
+ foo
26
+ end
27
+
28
+ post '/bar' do
29
+ params.to_json
30
+ end
31
+
32
+ post '/emit' do
33
+ websockets.each {|ws| ws.write TEST}
34
+ params.to_json
35
+ end
36
+
37
+ socket '/ws' do |s|
38
+ websockets << s
39
+ while msg = s.read
40
+ 5.times { s.write TEST }
41
+ s.write foo.to_json
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ Foo.run
48
+ ```
49
+
50
+ ### Tilt / ERB
51
+
52
+ To make `erb` available in route blocks
53
+
54
+ 1. add `tilt` to your `Gemfile`:
55
+
56
+ ```ruby
57
+ gem 'tilt'
58
+ ```
59
+
60
+ 2. require `angelo/tilt/erb`
61
+ 3. include `Angelo::Tilt::ERB` in your app
62
+
63
+ ```ruby
64
+ class Foo < Angelo::Base
65
+ include Angelo::Tilt::ERB
66
+
67
+ @@views = 'some/other/path' # defaults to './views'
68
+
69
+ get '/' do
70
+ erb :index
71
+ end
72
+
73
+ end
74
+ ```
75
+
76
+ ### License
77
+
78
+ see LICENSE
data/angelo.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/angelo/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kenichi Nakamura"]
6
+ gem.email = ["kenichi.nakamura@gmail.com"]
7
+ gem.description = gem.summary = "A Sinatra-esque DSL for Reel"
8
+ gem.homepage = "https://github.com/kenichi/angelo"
9
+ gem.files = `git ls-files | grep -Ev '^(myapp|examples)'`.split("\n")
10
+ gem.test_files = `git ls-files -- test/*`.split("\n")
11
+ gem.name = "angelo"
12
+ gem.require_paths = ["lib"]
13
+ gem.version = Angelo::VERSION
14
+ gem.license = 'apache'
15
+ gem.add_dependency 'reel'
16
+ end
@@ -0,0 +1,60 @@
1
+ $:.unshift File.expand_path '../../../lib', __FILE__
2
+
3
+ require 'angelo'
4
+ require 'angelo/tilt/erb'
5
+
6
+ class Foo < Angelo::Base
7
+ include Angelo::Tilt::ERB
8
+
9
+ TEST = {foo: "bar", baz: 123, bat: false}.to_json
10
+
11
+ def pong; 'pong'; end
12
+ def foo; params[:foo]; end
13
+ def time_ms; Time.now.to_f * 1000.0; end
14
+
15
+ before do
16
+ info "request: #{request.method} #{request.path}"
17
+ @foo = request.path
18
+ @timing = time_ms
19
+ end
20
+
21
+ after do
22
+ info "timing: #{time_ms - @timing}ms"
23
+ end
24
+
25
+ get '/' do
26
+ @name = params[:name]
27
+ @host = request.headers['Host']
28
+ erb :index, locals: {zzz: 'word'}
29
+ end
30
+
31
+ get '/ping' do
32
+ debug "@foo: #{@foo}"
33
+ pong
34
+ end
35
+
36
+ post '/foo' do
37
+ foo
38
+ end
39
+
40
+ post '/bar' do
41
+ content_type = :json
42
+ params
43
+ end
44
+
45
+ post '/emit' do
46
+ websockets.each {|ws| ws.write TEST}
47
+ params.to_json
48
+ end
49
+
50
+ socket '/ws' do |s|
51
+ websockets << s
52
+ while msg = s.read
53
+ 5.times { s.write TEST }
54
+ s.write foo.to_json
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ Foo.run unless $0 == 'irb'
@@ -0,0 +1,20 @@
1
+ hi <%= @name %><br/>
2
+ this is a local - <%= zzz %><br/>
3
+ <br/>
4
+ <a href="#" onclick="openws();">websocket!</a>
5
+ <br/>
6
+ <a href="#" onclick="emit();">emit!</a>
7
+
8
+ <script>
9
+ var ws;
10
+ function openws() {
11
+ ws = new WebSocket('ws://<%= @host %>/ws');
12
+ ws.onmessage = function(e) { console.log(e.data); };
13
+ ws.onopen = function(e) {
14
+ ws.send('hi');
15
+ };
16
+ }
17
+ function emit() {
18
+ $.post('/emit', {data: {"foo": "bar"}});
19
+ }
20
+ </script>
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>example</title>
5
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
6
+ </head>
7
+ <body>
8
+ <%= yield %>
9
+ </body>
10
+ </html>
@@ -0,0 +1,106 @@
1
+ module Angelo
2
+
3
+ class Base
4
+ include ParamsParser
5
+ include Celluloid::Logger
6
+
7
+ extend Forwardable
8
+ def_delegator :@responder, :request
9
+
10
+ attr_accessor :responder
11
+
12
+ class << self
13
+
14
+ attr_accessor :app_file
15
+
16
+ def inherited subclass
17
+ subclass.app_file = caller(1).map {|l| l.split(/:(?=|in )/, 3)[0,1]}.flatten[0]
18
+
19
+ def subclass.root
20
+ @root ||= File.expand_path '..', app_file
21
+ @root
22
+ end
23
+
24
+ def subclass.view_dir
25
+ v = self.class_variable_get(:@@views) rescue 'views'
26
+ File.join root, v
27
+ end
28
+
29
+ end
30
+
31
+ def compile! name, &block
32
+ define_method name, &block
33
+ method = instance_method name
34
+ remove_method name
35
+ method
36
+ end
37
+
38
+ def routes
39
+ @routes ||= {}
40
+ [:get, :post, :put, :delete, :options, :socket].each do |m|
41
+ @routes[m] ||= {}
42
+ end
43
+ @routes
44
+ end
45
+
46
+ def before &block
47
+ # @before = compile! :before, &block
48
+ define_method :before, &block
49
+ end
50
+
51
+ def after &block
52
+ # @after = compile! :after, &block
53
+ define_method :after, &block
54
+ end
55
+
56
+ [:get, :post, :put, :delete, :options].each do |m|
57
+ define_method m do |path, &block|
58
+ routes[m][path] = Responder.new &block
59
+ end
60
+ end
61
+
62
+ def socket path, &block
63
+ routes[:socket][path] = WebsocketResponder.new &block
64
+ end
65
+
66
+ def websockets
67
+ if @websockets.nil?
68
+ @websockets = []
69
+ def @websockets.each &block
70
+ super do |ws|
71
+ begin
72
+ yield ws
73
+ rescue Reel::SocketError => rse
74
+ warn "#{rse.class} - #{rse.message}"
75
+ delete ws
76
+ end
77
+ end
78
+ end
79
+ end
80
+ @websockets.reject! &:closed?
81
+ @websockets
82
+ end
83
+
84
+ def run host = '127.0.0.1', port = 4567
85
+ @server = Angelo::Server.new self, host, port
86
+ sleep
87
+ end
88
+
89
+ end
90
+
91
+ def before; end;
92
+ def after; end;
93
+
94
+ def params
95
+ @params ||= case request.method
96
+ when GET; parse_query_string
97
+ when POST; parse_post_body
98
+ end
99
+ @params
100
+ end
101
+
102
+ def websockets; self.class.websockets; end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,48 @@
1
+ require 'cgi'
2
+
3
+ module Angelo
4
+
5
+ module ParamsParser
6
+
7
+ EMPTY_JSON = '{}'.freeze
8
+ SEMICOLON = ';'.freeze
9
+
10
+ def parse_formencoded str
11
+ str.split('&').reduce(Responder.symhash) do |p, kv|
12
+ key, value = kv.split('=').map {|s| CGI.escape s}
13
+ p[key] = value
14
+ p
15
+ end
16
+ end
17
+
18
+ def parse_query_string
19
+ parse_formencoded(request.query_string || '')
20
+ end
21
+
22
+ def parse_post_body
23
+ body = request.body.to_s
24
+ case
25
+ when form_encoded?
26
+ body = parse_formencoded body
27
+ when json?
28
+ body = EMPTY_JSON if body.empty?
29
+ body = JSON.parse body
30
+ end
31
+ parse_query_string.merge! body
32
+ end
33
+
34
+ def form_encoded?
35
+ content_type? FORM_TYPE
36
+ end
37
+
38
+ def json?
39
+ content_type? JSON_TYPE
40
+ end
41
+
42
+ def content_type? type
43
+ request.headers[CONTENT_TYPE_HEADER_KEY].split(SEMICOLON).include? type
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,38 @@
1
+ module Angelo
2
+
3
+ class WebsocketResponder < Responder
4
+
5
+ def params
6
+ @params ||= parse_query_string
7
+ @params
8
+ end
9
+
10
+ def request= request
11
+ @params = nil
12
+ @request = request
13
+ @websocket = @request.websocket
14
+ handle_request
15
+ end
16
+
17
+ def handle_request
18
+ begin
19
+ if @response_handler
20
+ @bound_response_handler ||= @response_handler.bind @base
21
+ @bound_response_handler[@websocket]
22
+ else
23
+ raise NotImplementedError
24
+ end
25
+ rescue IOError => ioe
26
+ error "#{ioe.class} - #{ioe.message}"
27
+ @websocket.close
28
+ @base.websockets.delete @websocket
29
+ rescue => e
30
+ error e.message
31
+ ::STDERR.puts e.backtrace
32
+ @connection.close
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,87 @@
1
+ module Angelo
2
+
3
+ class Responder
4
+ include Celluloid::Logger
5
+
6
+ class << self
7
+
8
+ def symhash
9
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
10
+ end
11
+
12
+ end
13
+
14
+ attr_writer :connection
15
+ attr_reader :request
16
+
17
+ def initialize &block
18
+ @response_handler = Base.compile! :request_handler, &block
19
+ end
20
+
21
+ def base= base
22
+ @base = base
23
+ @base.responder = self
24
+ end
25
+
26
+ def request= request
27
+ @params = nil
28
+ @request = request
29
+ handle_request
30
+ respond
31
+ end
32
+
33
+ def handle_request
34
+ begin
35
+ if @response_handler
36
+ @base.before
37
+ @body = @response_handler.bind(@base).call || ''
38
+ @base.after
39
+ else
40
+ raise NotImplementedError
41
+ end
42
+ rescue => e
43
+ error_message = case
44
+ when respond_with?(:json)
45
+ { error: e.message }.to_json
46
+ else
47
+ e.message
48
+ end
49
+ @connection.respond :internal_server_error, headers, error_message
50
+ @connection.close
51
+ error "#{e.class} - #{e.message}"
52
+ ::STDERR.puts e.backtrace
53
+ end
54
+ end
55
+
56
+ def headers hs = nil
57
+ @headers ||= {}
58
+ @headers.merge! hs if hs
59
+ @headers
60
+ end
61
+
62
+ def content_type= type
63
+ case type
64
+ when :json
65
+ headers CONTENT_TYPE_HEADER_KEY => JSON_TYPE
66
+ else
67
+ raise ArgumentError.new "invalid content_type: #{type}"
68
+ end
69
+ end
70
+
71
+ def respond_with? type
72
+ case headers[CONTENT_TYPE_HEADER_KEY]
73
+ when JSON_TYPE
74
+ type == :json
75
+ else
76
+ type == :html
77
+ end
78
+ end
79
+
80
+ def respond
81
+ @body = @body.to_json if respond_with? :json
82
+ @connection.respond :ok, DEFAULT_RESPONSE_HEADERS.merge(headers), @body
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,47 @@
1
+ module Angelo
2
+
3
+ class Server < Reel::Server
4
+ include Celluloid::Logger
5
+
6
+ def initialize base, host = '127.0.0.1', port = 4567
7
+ @base = base
8
+ info "Angelo listening on #{host}:#{port}"
9
+ super host, port, &method(:on_connection)
10
+ end
11
+
12
+ def on_connection connection
13
+ connection.each_request do |request|
14
+ if request.websocket?
15
+ debug "got websocket request..."
16
+ route_websocket connection, request
17
+ else
18
+ route_request connection, request
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def route_request connection, request
26
+ route! request.method.downcase.to_sym, connection, request
27
+ end
28
+
29
+ def route_websocket connection, request
30
+ route! :socket, connection, request
31
+ end
32
+
33
+ def route! meth, connection, request
34
+ rs = @base.routes[meth][request.path]
35
+ if rs
36
+ responder = rs.dup
37
+ responder.base = @base.new
38
+ responder.connection = connection
39
+ responder.request = request
40
+ else
41
+ connection.respond :not_found, DEFAULT_RESPONSE_HEADERS, NOT_FOUND
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,76 @@
1
+ require 'erb'
2
+ require 'tilt'
3
+
4
+ module Angelo
5
+ module Tilt
6
+ module ERB
7
+
8
+ # hrm, sneaky
9
+ #
10
+ def self.included base
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ DEFAULT_LAYOUT = 'layout.html.erb'.freeze
17
+
18
+ def view_glob *glob
19
+ File.join view_dir, *glob
20
+ end
21
+
22
+ def templatify *glob
23
+ Dir[view_glob *glob].reduce({}) do |h,v|
24
+ sym = v.gsub view_dir + '/', ''
25
+ return h if (block_given? && yield(v))
26
+ sym.gsub! '/', '_'
27
+ sym.gsub! /\.\w+?\.erb$/, ''
28
+ h[sym.to_sym] = ::Tilt::ERBTemplate.new v
29
+ h
30
+ end
31
+ end
32
+
33
+ def templates
34
+ @templates ||= templatify('**', '*.erb'){|v| v =~ /^layouts\//}
35
+ @templates
36
+ end
37
+
38
+ def layout_templates
39
+ @layout_templates ||= templatify 'layouts', '*.erb'
40
+ @layout_templates
41
+ end
42
+
43
+ def default_layout
44
+ if @default_layout.nil?
45
+ l = view_glob(DEFAULT_LAYOUT)
46
+ @default_layout = ::Tilt::ERBTemplate.new l if File.exist? l
47
+ end
48
+ @default_layout
49
+ end
50
+
51
+ end
52
+
53
+ def erb view, opts = {locals: {}}
54
+ locals = Hash === opts[:locals] ? opts[:locals] : {}
55
+ render = ->{self.class.templates[view].render self, locals}
56
+ case opts[:layout]
57
+ when false
58
+ render[]
59
+ when Symbol
60
+ if lt = self.class.layout_templates[opts[:layout]]
61
+ lt.render self, locals, &render
62
+ else
63
+ raise ArgumentError.new "unknown layout - :#{opts[:layout]}"
64
+ end
65
+ else
66
+ if self.class.default_layout
67
+ self.class.default_layout.render self, locals, &render
68
+ else
69
+ render[]
70
+ end
71
+ end
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Angelo
2
+ VERSION = '0.0.4'
3
+ end
data/lib/angelo.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'reel'
2
+ require 'json'
3
+ require 'pry'
4
+ require 'pry-nav'
5
+
6
+ module Angelo
7
+
8
+ GET = 'GET'.freeze
9
+ POST = 'POST'.freeze
10
+ PUT = 'PUT'.freeze
11
+ DELETE = 'DELETE'.freeze
12
+ OPTIONS = 'OPTIONS'.freeze
13
+
14
+ CONTENT_TYPE_HEADER_KEY = 'Content-Type'.freeze
15
+
16
+ HTML_TYPE = 'text/html'.freeze
17
+ JSON_TYPE = 'application/json'.freeze
18
+ FORM_TYPE = 'application/x-www-form-urlencoded'.freeze
19
+
20
+ DEFAULT_RESPONSE_HEADERS = {
21
+ CONTENT_TYPE_HEADER_KEY => HTML_TYPE
22
+ }
23
+
24
+ NOT_FOUND = 'Not Found'.freeze
25
+
26
+ end
27
+
28
+ require 'angelo/version'
29
+ require 'angelo/params_parser'
30
+ require 'angelo/server'
31
+ require 'angelo/base'
32
+ require 'angelo/responder'
33
+ require 'angelo/responder/websocket'
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: angelo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kenichi Nakamura
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: reel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A Sinatra-esque DSL for Reel
31
+ email:
32
+ - kenichi.nakamura@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - CHANGELOG
39
+ - Gemfile
40
+ - LICENSE
41
+ - README.md
42
+ - angelo.gemspec
43
+ - example/foo/foo.rb
44
+ - example/foo/views/index.html.erb
45
+ - example/foo/views/layout.html.erb
46
+ - lib/angelo.rb
47
+ - lib/angelo/base.rb
48
+ - lib/angelo/params_parser.rb
49
+ - lib/angelo/responder.rb
50
+ - lib/angelo/responder/websocket.rb
51
+ - lib/angelo/server.rb
52
+ - lib/angelo/tilt/erb.rb
53
+ - lib/angelo/version.rb
54
+ homepage: https://github.com/kenichi/angelo
55
+ licenses:
56
+ - apache
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.23
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A Sinatra-esque DSL for Reel
79
+ test_files: []