angelo 0.0.4

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/.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: []