rasti-web 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7421139482ecc1b7964db860e548a5805eb4fc66
4
+ data.tar.gz: 573b00b2c7da729862711044b6a62cad8bc3bad0
5
+ SHA512:
6
+ metadata.gz: 493938ffdf31f07406316a026096a00bf7ba3554c50dfbe4ec820d3676a663520343a40fd10521d3d968adda48f78b1a770375e8b5bb92098891a439938b0116
7
+ data.tar.gz: dc2e21807b7ceb4f6be33f21352f262444bf07f111c308b0c6de95876f61053b40200fe5c98fe77f168a68aa9c58ff34703a6bc82ba9058f8766bff6596f4de0
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ service_name: travis-ci
2
+ repo_token: m9idBSHE2hpi7JYV14vDKnOsWV9xW00Yf
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rasti-web
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0
5
+ - 2.1
6
+ - jruby
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rasti-web.gemspec
4
+ gemspec
5
+
6
+ gem 'coveralls', require: false
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Gabriel Naiman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Rasti::Web
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/rasti-web.png)](https://rubygems.org/gems/rasti-web)
4
+ [![Build Status](https://travis-ci.org/gabynaiman/rasti-web.png?branch=master)](https://travis-ci.org/gabynaiman/rasti-web)
5
+ [![Coverage Status](https://coveralls.io/repos/gabynaiman/rasti-web/badge.png?branch=master)](https://coveralls.io/r/gabynaiman/rasti-web?branch=master)
6
+ [![Code Climate](https://codeclimate.com/github/gabynaiman/rasti-web.png)](https://codeclimate.com/github/gabynaiman/rasti-web)
7
+ [![Dependency Status](https://gemnasium.com/gabynaiman/rasti-web.png)](https://gemnasium.com/gabynaiman/rasti-web)
8
+
9
+ Web blocks to build robust applications
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'rasti-web'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install rasti-web
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it ( https://github.com/gabynaiman/rasti-web/fork )
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:spec) do |t|
5
+ t.libs << 'spec'
6
+ t.pattern = 'spec/**/*_spec.rb'
7
+ t.verbose = false
8
+ end
9
+
10
+ task default: :spec
11
+
12
+ desc 'Pry console'
13
+ task :console do
14
+ require 'rasti-web'
15
+ require 'pry'
16
+ ARGV.clear
17
+ Pry.start
18
+ end
@@ -0,0 +1,52 @@
1
+ module Rasti
2
+ module Web
3
+ class Application
4
+ class << self
5
+
6
+ Router::VERBS.each do |verb|
7
+ define_method verb do |*args, &block|
8
+ router.public_send verb, *args, &block
9
+ end
10
+ end
11
+
12
+ def not_found(*args, &block)
13
+ router.not_found *args, &block
14
+ end
15
+
16
+ def use(*args, &block)
17
+ rack.use *args, &block
18
+ end
19
+
20
+ def map(path, endpoint=nil, &block)
21
+ rack.map path do
22
+ run endpoint || Endpoint.new(&block)
23
+ end
24
+ end
25
+
26
+ def call(env)
27
+ app.call env
28
+ end
29
+
30
+ private
31
+
32
+ def router
33
+ @router ||= Router.new
34
+ end
35
+
36
+ def rack
37
+ @rack ||= Rack::Builder.new
38
+ end
39
+
40
+ def app
41
+ @app ||= to_app
42
+ end
43
+
44
+ def to_app
45
+ rack.run router
46
+ rack.to_app
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,48 @@
1
+ module Rasti
2
+ module Web
3
+ class Controller
4
+
5
+ extend Forwardable
6
+
7
+ def_delegators :request, :params, :session
8
+ def_delegator :response, :redirect, :redirect_to
9
+
10
+ attr_reader :request, :response, :render
11
+
12
+ def initialize(request, response, render)
13
+ @request = request
14
+ @response = response
15
+ @render = render
16
+ end
17
+
18
+ def execute(action_name)
19
+ public_send action_name
20
+ rescue => ex
21
+ if respond_to? ex.class.name
22
+ public_send ex.class.name, ex
23
+ else
24
+ raise ex
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def action(action_name)
30
+ raise "Undefined action '#{action_name}' in #{name}" unless instance_methods.include? action_name.to_sym
31
+
32
+ Endpoint.new do |req, res, render|
33
+ new(req, res, render).execute(action_name)
34
+ end
35
+ end
36
+
37
+ alias_method :>>, :action
38
+
39
+ def rescue_from(exception_class, &block)
40
+ define_method exception_class.name do |ex|
41
+ instance_exec ex, &block
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ module Rasti
2
+ module Web
3
+ class Endpoint
4
+
5
+ def initialize(&block)
6
+ @block = block
7
+ end
8
+
9
+ def call(env)
10
+ request = Request.new env
11
+ response = Rack::Response.new
12
+ @block.call request, response, Render.new(request, response)
13
+ response.finish
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,82 @@
1
+ module Rasti
2
+ module Web
3
+ class Render
4
+
5
+ attr_reader :request, :response, :view_context
6
+
7
+ def initialize(request, response)
8
+ @request = request
9
+ @response = response
10
+ @view_context = ViewContext.new request, response
11
+ end
12
+
13
+ def status(status, *args)
14
+ respond_with status,
15
+ extract_headers(args),
16
+ extract_body(args)
17
+ end
18
+
19
+ def text(text, *args)
20
+ respond_with extract_status(args),
21
+ extract_headers(args).merge('Content-Type' => 'text/plain'),
22
+ text
23
+ end
24
+
25
+ def html(html, *args)
26
+ respond_with extract_status(args),
27
+ extract_headers(args).merge('Content-Type' => 'text/html'),
28
+ html
29
+ end
30
+
31
+ def json(object, *args)
32
+ respond_with extract_status(args),
33
+ extract_headers(args).merge('Content-Type' => 'application/json'),
34
+ object.is_a?(String) ? object : JSON.dump(object)
35
+ end
36
+
37
+ def js(script, *args)
38
+ respond_with extract_status(args),
39
+ extract_headers(args).merge('Content-Type' => 'application/javascript'),
40
+ script
41
+ end
42
+
43
+ def partial(template, locals={})
44
+ response['Content-Type'] = 'text/html'
45
+ response.write view_context.render(template, locals)
46
+ end
47
+
48
+ def layout(template=nil, &block)
49
+ content = block.call if block
50
+ layout = view_context.render(template || Web.default_layout) { content }
51
+
52
+ response['Content-Type'] = 'text/html'
53
+ response.write layout
54
+ end
55
+
56
+ def view(template, locals={}, layout_template=nil)
57
+ layout(layout_template) { view_context.render template, locals }
58
+ end
59
+
60
+ private
61
+
62
+ def respond_with(status, headers, body)
63
+ response.status = status if status
64
+ response.headers.merge! headers
65
+ response.write body if body
66
+ end
67
+
68
+ def extract_status(args)
69
+ args.detect { |a| a.is_a? Fixnum }
70
+ end
71
+
72
+ def extract_headers(args)
73
+ args.detect { |a| a.is_a? Hash } || {}
74
+ end
75
+
76
+ def extract_body(args)
77
+ args.detect { |a| a.is_a? String }
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,25 @@
1
+ module Rasti
2
+ module Web
3
+ class Request < Rack::Request
4
+
5
+ def initialize(env)
6
+ super
7
+ params.merge! JSON.parse(body_text) if json? && body_text
8
+ params.merge! env[ROUTE_PARAMS] if env[ROUTE_PARAMS]
9
+ end
10
+
11
+ def body_text
12
+ @body_text ||= begin
13
+ text = body.read
14
+ body.rewind
15
+ text
16
+ end
17
+ end
18
+
19
+ def json?
20
+ content_type == 'application/json'
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module Rasti
2
+ module Web
3
+ class Route
4
+
5
+ attr_reader :pattern, :endpoint, :regexp, :params
6
+
7
+ def initialize(pattern, endpoint=nil, &block)
8
+ @pattern = normalize pattern
9
+ @endpoint = endpoint || Endpoint.new(&block)
10
+ compile
11
+ end
12
+
13
+ def match?(path)
14
+ !regexp.match(normalize(path)).nil?
15
+ end
16
+
17
+ def extract_params(path)
18
+ result = regexp.match path
19
+ result ? Hash[params.zip(result.captures)] : {}
20
+ end
21
+
22
+ private
23
+
24
+ def compile
25
+ @params = []
26
+ regexp = pattern.gsub(/(:\w+)/) do |match|
27
+ @params << match[1..-1]
28
+ "([^/?#]+)"
29
+ end
30
+ @regexp = %r{^#{regexp}$}
31
+ end
32
+
33
+ def normalize(path)
34
+ return '/' if path.strip.empty? || path == '/'
35
+ return path[0..-2] if path[-1, 1] == '/'
36
+ path
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ module Rasti
2
+ module Web
3
+ class Router
4
+
5
+ VERBS = %w(delete get head options patch post put).freeze
6
+
7
+ NOT_FOUND_PATTERN = '/404'
8
+
9
+ VERBS.each do |verb|
10
+ define_method verb do |pattern, endpoint=nil, &block|
11
+ routes[verb] << Route.new(pattern, endpoint, &block)
12
+ end
13
+ end
14
+
15
+ def not_found(endpoint=nil, &block)
16
+ @not_found_route = Route.new(NOT_FOUND_PATTERN, endpoint, &block)
17
+ end
18
+
19
+ def call(env)
20
+ route = route_for env
21
+ env[ROUTE_PARAMS] = route.extract_params env['PATH_INFO']
22
+ route.endpoint.call env
23
+ end
24
+
25
+ private
26
+
27
+ def routes
28
+ @routes ||= Hash.new { |h,k| h[k] = [] }
29
+ end
30
+
31
+ def route_for(env)
32
+ routes[env['REQUEST_METHOD'].downcase].detect { |r| r.match? env['PATH_INFO'] } || not_found_route
33
+ end
34
+
35
+ def not_found_route
36
+ @not_found_route ||= Route.new NOT_FOUND_PATTERN do |request, response, render|
37
+ render.status 404, "Not found: #{request.request_method} #{request.path_info}"
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module Rasti
2
+ module Web
3
+ class Template
4
+
5
+ def self.render(template, context=nil, locals={}, &block)
6
+ files = Web.template_engines.map { |e| File.join Web.views_path, "#{template}.#{e}" }
7
+ template_file = files.detect { |f| File.exists? f }
8
+
9
+ raise "Missing template #{template} [#{files.join(', ')}]" unless template_file
10
+
11
+ tilt = cache.fetch(template_file) { Tilt.new template_file }
12
+ tilt.render(context, locals, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def self.cache
18
+ Thread.current[:templates_cache] ||= Tilt::Cache.new
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module Rasti
2
+ module Web
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ module Rasti
2
+ module Web
3
+ class ViewContext
4
+
5
+ attr_reader :request, :response
6
+
7
+ def initialize(request, response)
8
+ @request = request
9
+ @response = response
10
+ end
11
+
12
+ def render(template, locals={}, &block)
13
+ Template.render template, self, locals, &block
14
+ end
15
+
16
+ end
17
+ end
18
+ end
data/lib/rasti/web.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'rack'
2
+ require 'tilt'
3
+ require 'json'
4
+ require 'class_config'
5
+ require 'forwardable'
6
+
7
+ require_relative 'web/route'
8
+ require_relative 'web/router'
9
+ require_relative 'web/endpoint'
10
+ require_relative 'web/application'
11
+ require_relative 'web/template'
12
+ require_relative 'web/view_context'
13
+ require_relative 'web/render'
14
+ require_relative 'web/request'
15
+ require_relative 'web/controller'
16
+ require_relative 'web/version'
17
+
18
+ module Rasti
19
+ module Web
20
+ ROUTE_PARAMS = 'rack.request.route_params'
21
+
22
+ extend ClassConfig
23
+
24
+ attr_config :views_path, File.join(Dir.pwd, 'views')
25
+ attr_config :template_engines, [:erb]
26
+ attr_config :default_layout, 'layout'
27
+ attr_config :helpers, []
28
+
29
+ after_config do |config|
30
+ config.helpers.each { |h| ViewContext.send :include, h }
31
+ end
32
+ end
33
+ end
data/lib/rasti-web.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'rasti/web'
data/rasti-web.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rasti/web/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'rasti-web'
8
+ spec.version = Rasti::Web::VERSION
9
+ spec.authors = ['Gabriel Naiman']
10
+ spec.email = ['gabynaiman@gmail.com']
11
+ spec.summary = 'Web blocks to build robust applications'
12
+ spec.description = 'Web blocks to build robust applications'
13
+ spec.homepage = 'https://github.com/gabynaiman/rasti-web'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'rack'
22
+ spec.add_dependency 'tilt'
23
+ spec.add_dependency 'class_config', '~> 0.0.2'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'minitest', '~> 4.7'
28
+ spec.add_development_dependency 'turn', '~> 0.9'
29
+ spec.add_development_dependency 'simplecov'
30
+ spec.add_development_dependency 'pry'
31
+ spec.add_development_dependency 'rack-test'
32
+ end
@@ -0,0 +1,77 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestMiddleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env['PATH_INFO'] == '/private'
10
+ [403, {}, ['Permission denied']]
11
+ else
12
+ @app.call env
13
+ end
14
+ end
15
+ end
16
+
17
+ class TestMap < Rasti::Web::Application
18
+ get '/resource/:id' do |request, response, render|
19
+ render.json id: request.params['id'].to_i
20
+ end
21
+ end
22
+
23
+ class TestApp < Rasti::Web::Application
24
+
25
+ use TestMiddleware
26
+
27
+ map '/api', TestMap
28
+
29
+ get '/' do |request, response, render|
30
+ render.html 'Page content'
31
+ end
32
+
33
+ not_found do |request, response, render|
34
+ render.status 404, 'Page not found'
35
+ end
36
+
37
+ end
38
+
39
+ describe Rasti::Web::Application do
40
+
41
+ include Rack::Test::Methods
42
+
43
+ def app
44
+ TestApp
45
+ end
46
+
47
+ it 'Defined route' do
48
+ get '/'
49
+
50
+ last_response.status.must_equal 200
51
+ last_response['Content-Type'].must_equal 'text/html'
52
+ last_response.body.must_equal 'Page content'
53
+ end
54
+
55
+ it 'Not found' do
56
+ get '/not_found'
57
+
58
+ last_response.status.must_equal 404
59
+ last_response.body.must_equal 'Page not found'
60
+ end
61
+
62
+ it 'Middleware' do
63
+ get '/private'
64
+
65
+ last_response.status.must_equal 403
66
+ last_response.body.must_equal 'Permission denied'
67
+ end
68
+
69
+ it 'Map' do
70
+ get '/api/resource/123'
71
+
72
+ last_response.status.must_equal 200
73
+ last_response['Content-Type'].must_equal 'application/json'
74
+ last_response.body.must_equal '{"id":123}'
75
+ end
76
+
77
+ end