kawaii-core 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +372 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/controller.ru +30 -0
- data/examples/hello_world.rb +5 -0
- data/examples/hello_world.ru +4 -0
- data/examples/modular.ru +11 -0
- data/examples/modular/first_app.rb +6 -0
- data/examples/modular/second_app.rb +6 -0
- data/examples/nested_routes.rb +11 -0
- data/examples/views.ru +10 -0
- data/examples/views/index.html.erb +1 -0
- data/kawaii-core.gemspec +34 -0
- data/lib/kawaii.rb +17 -0
- data/lib/kawaii/base.rb +48 -0
- data/lib/kawaii/controller.rb +32 -0
- data/lib/kawaii/core_ext/hash.rb +23 -0
- data/lib/kawaii/core_ext/string.rb +6 -0
- data/lib/kawaii/matchers.rb +135 -0
- data/lib/kawaii/method_chain.rb +15 -0
- data/lib/kawaii/render_methods.rb +14 -0
- data/lib/kawaii/route.rb +27 -0
- data/lib/kawaii/route_context.rb +49 -0
- data/lib/kawaii/route_handler.rb +54 -0
- data/lib/kawaii/route_mapping.rb +35 -0
- data/lib/kawaii/routing_methods.rb +120 -0
- data/lib/kawaii/server_methods.rb +33 -0
- data/lib/kawaii/singleton_app.rb +58 -0
- data/lib/kawaii/version.rb +4 -0
- metadata +193 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'kawaii'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require 'pry'
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'kawaii'
|
2
|
+
|
3
|
+
class HelloWorld < Kawaii::Controller
|
4
|
+
def index
|
5
|
+
@title = 'Hello, world'
|
6
|
+
render('index.html.erb')
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
"GET /users/#{params[:id]}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
'POST /users'
|
15
|
+
end
|
16
|
+
|
17
|
+
def update
|
18
|
+
"PATCH /users/#{params[:id]}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
"DELETE /users/#{params[:id]}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class App < Kawaii::Base
|
27
|
+
route '/users/', :hello_world
|
28
|
+
end
|
29
|
+
|
30
|
+
run App
|
data/examples/modular.ru
ADDED
data/examples/views.ru
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<h1><%= @title %></h1>
|
data/kawaii-core.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'kawaii/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'kawaii-core'
|
8
|
+
spec.version = Kawaii::VERSION
|
9
|
+
spec.authors = ['Marcin Bilski']
|
10
|
+
spec.email = ['gyamtso@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Kawaii is a simple web framework based on Rack'
|
13
|
+
spec.description = 'Kawaii is a basic but extensible web framework based on Rack'
|
14
|
+
|
15
|
+
spec.homepage = "https://github.com/bilus/kawaii"
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
}
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'rack', '~> 1.6'
|
26
|
+
spec.add_dependency 'tilt', '~> 2.0'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.4'
|
31
|
+
spec.add_development_dependency 'guard-rspec', '~>4.6'
|
32
|
+
spec.add_development_dependency 'rack-test', '~>0.6'
|
33
|
+
spec.add_development_dependency 'yard', '~> 0.8'
|
34
|
+
end
|
data/lib/kawaii.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'kawaii/version'
|
2
|
+
require 'rack'
|
3
|
+
require 'tilt'
|
4
|
+
require 'kawaii/core_ext/hash'
|
5
|
+
require 'kawaii/core_ext/string'
|
6
|
+
require 'kawaii/method_chain'
|
7
|
+
require 'kawaii/render_methods'
|
8
|
+
require 'kawaii/matchers'
|
9
|
+
require 'kawaii/route_mapping'
|
10
|
+
require 'kawaii/route_handler'
|
11
|
+
require 'kawaii/route'
|
12
|
+
require 'kawaii/routing_methods'
|
13
|
+
require 'kawaii/route_context'
|
14
|
+
require 'kawaii/server_methods'
|
15
|
+
require 'kawaii/base'
|
16
|
+
require 'kawaii/singleton_app'
|
17
|
+
require 'kawaii/controller'
|
data/lib/kawaii/base.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Kawaii
|
2
|
+
# Base class for all Kawaii applications. Inherit from this class to create
|
3
|
+
# a modular application.
|
4
|
+
#
|
5
|
+
# @example my_app.rb
|
6
|
+
# require 'kawaii'
|
7
|
+
# class MyApp < Kawaii::Base
|
8
|
+
# get '/' do
|
9
|
+
# 'Hello, world'
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# @example config.ru
|
14
|
+
# require 'my_app.rb'
|
15
|
+
# run MyApp
|
16
|
+
class Base
|
17
|
+
def initialize(downstream_app = nil)
|
18
|
+
@downstream_app = downstream_app
|
19
|
+
end
|
20
|
+
|
21
|
+
# Instances of classes derived from [Kawaii::Base] are Rack applications.
|
22
|
+
def call(env)
|
23
|
+
matching = self.class.match(env) || not_found
|
24
|
+
matching.call(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
include ServerMethods
|
29
|
+
include RoutingMethods
|
30
|
+
|
31
|
+
# Make it runnable via `run MyApp`.
|
32
|
+
def call(env)
|
33
|
+
@app ||= new
|
34
|
+
@app.call(env)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def not_found
|
41
|
+
@downstream_app || ->(_env) { text(404, 'Not found') }
|
42
|
+
end
|
43
|
+
|
44
|
+
def text(status, s)
|
45
|
+
[status, { Rack::CONTENT_TYPE => 'text/plain' }, [s]]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Kawaii
|
2
|
+
# MVP controller. Define actions and map to them using regular routing
|
3
|
+
# functions.
|
4
|
+
#
|
5
|
+
# @example Routing to controllers
|
6
|
+
# class HelloWorld < Kawaii::Controller
|
7
|
+
# def index
|
8
|
+
# 'Hello, world'
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# def show
|
12
|
+
# @id = params[:id]
|
13
|
+
# render('show.html.erb')
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# get '/', 'hello_world#index'
|
18
|
+
class Controller
|
19
|
+
include RenderMethods
|
20
|
+
|
21
|
+
# Parameter [Hash] accessible in actions
|
22
|
+
attr_reader :params
|
23
|
+
# Rack::Request accessible in actions
|
24
|
+
attr_reader :request
|
25
|
+
|
26
|
+
# Creates a controller.
|
27
|
+
def initialize(params, request)
|
28
|
+
@params = params
|
29
|
+
@request = request
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Extend {Hash}.
|
2
|
+
class Hash
|
3
|
+
# Transforms keys.
|
4
|
+
#
|
5
|
+
# @yield [key] gives each key to the block
|
6
|
+
# @yieldreturn [key] new key
|
7
|
+
#
|
8
|
+
# @return [Hash] new hash with transformed keys
|
9
|
+
def update_keys
|
10
|
+
result = self.class.new
|
11
|
+
each_key do |key|
|
12
|
+
result[yield(key)] = self[key]
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
# Turns string keys to symbols.
|
18
|
+
#
|
19
|
+
# @return [Hash] a new hash
|
20
|
+
def symbolize_keys
|
21
|
+
update_keys(&:to_sym)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# Route matchers.
|
2
|
+
module Kawaii
|
3
|
+
# Result of matching a path.
|
4
|
+
class Match
|
5
|
+
# What is left of the actual path after {Matcher#match} consumed the
|
6
|
+
# matching portion.
|
7
|
+
attr_reader :remaining_path
|
8
|
+
# Hash containing params extracted from paths such as
|
9
|
+
# /users/:user_id/posts/:post_id
|
10
|
+
attr_reader :params
|
11
|
+
|
12
|
+
# Creates a new match result.
|
13
|
+
# @param remaining_path [String] what is left of the actual path after
|
14
|
+
# {Matcher#match} consumed the matching portion
|
15
|
+
# @param params [Hash] params extracted from paths such as /users/:id
|
16
|
+
def initialize(remaining_path, params = {})
|
17
|
+
@remaining_path = remaining_path
|
18
|
+
@params = params
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @abstract Base class for a route matcher.
|
23
|
+
class Matcher
|
24
|
+
# Creates a matcher.
|
25
|
+
# @param path [String, Regexp, Matcher] path specification to compile
|
26
|
+
# to a matcher
|
27
|
+
# @param options [Hash] :full_match to require the entire path to match
|
28
|
+
# the resulting matcher
|
29
|
+
# Generated matchers by default match only the beginning of the string
|
30
|
+
# containing the actual path from Rack environment.
|
31
|
+
def self.compile(path, options = {})
|
32
|
+
# @todo Make it extendable?
|
33
|
+
matcher = if path.is_a?(String)
|
34
|
+
StringMatcher.new(path)
|
35
|
+
elsif path.is_a?(Regexp)
|
36
|
+
RegexpMatcher.new(path)
|
37
|
+
elsif path.is_a?(Matcher)
|
38
|
+
path
|
39
|
+
else
|
40
|
+
fail RuntimeException, "#{path} is not a supported matcher"
|
41
|
+
end
|
42
|
+
options[:full_match] ? FullMatcher.new(matcher) : matcher
|
43
|
+
end
|
44
|
+
|
45
|
+
# Tries to match the actual path.
|
46
|
+
# @param path [String] the actual path from Rack env
|
47
|
+
# @return {Match} if the beginning of path does match or nil if there is
|
48
|
+
# no match.
|
49
|
+
def match(_path)
|
50
|
+
fail NotImplementedError
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Matcher for string paths. Supports named params.
|
55
|
+
#
|
56
|
+
# @example Simple string matcher
|
57
|
+
# get '/users' do ... end
|
58
|
+
#
|
59
|
+
# @example Named parameters
|
60
|
+
# get '/users/:id' do ... end
|
61
|
+
#
|
62
|
+
# @example Wildcards
|
63
|
+
# get '/users/?' do ... end # Optional trailing slash
|
64
|
+
class StringMatcher
|
65
|
+
# Creates a {StringMatcher}
|
66
|
+
# @param path [String] path specification
|
67
|
+
def initialize(path)
|
68
|
+
@rx = compile(path)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Tries to match the actual path.
|
72
|
+
# @param path [String] the actual path from Rack env
|
73
|
+
# @return {Match} if the beginning of path does match or nil if there
|
74
|
+
# is no match.
|
75
|
+
def match(path)
|
76
|
+
m = path.match(@rx)
|
77
|
+
Match.new(remaining_path(path, m), match_to_params(m)) if m
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def compile(path)
|
83
|
+
prep_path = path.gsub('*', '.*').gsub(%r{/\:([^/]+)}, '/(?<\1>[^\/]+)')
|
84
|
+
Regexp.new("^#{prep_path}")
|
85
|
+
end
|
86
|
+
|
87
|
+
def remaining_path(path, m)
|
88
|
+
_, start = m.offset(0) # Whole match.
|
89
|
+
path[start..-1]
|
90
|
+
end
|
91
|
+
|
92
|
+
def match_to_params(m)
|
93
|
+
# In-place reduce:
|
94
|
+
m.names.each_with_object({}) { |n, params| params[n.to_sym] = m[n] }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Regular expression matcher.
|
99
|
+
# @example Simple regular expression matcher
|
100
|
+
# get /\/users.*/ do ... end
|
101
|
+
class RegexpMatcher
|
102
|
+
# Creates a {RegexpMatcher}
|
103
|
+
# @param path [Regexp] path specification regex
|
104
|
+
# @todo Support parameters based on named capture groups.
|
105
|
+
def initialize(rx)
|
106
|
+
@rx = rx
|
107
|
+
end
|
108
|
+
|
109
|
+
# Tries to match the actual path.
|
110
|
+
# @param path [String] the actual path from Rack env
|
111
|
+
# @return {Match} if the beginning of path does match or nil if there is
|
112
|
+
# no match.
|
113
|
+
def match(path)
|
114
|
+
new_path = path.gsub(@rx, '')
|
115
|
+
Match.new(new_path) if path != new_path
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Ensures the entire path is consumed by the wrapped {Matcher} instance.
|
120
|
+
class FullMatcher
|
121
|
+
# Creates a {FullMatcher}.
|
122
|
+
# @param matcher [Matcher] wrapped matcher
|
123
|
+
def initialize(matcher)
|
124
|
+
@matcher = matcher
|
125
|
+
end
|
126
|
+
|
127
|
+
# Tries to match the entire actual path.
|
128
|
+
# @param path [String] the actual path from Rack env
|
129
|
+
# @return {Match} if the entire path does match or nil otherwise.
|
130
|
+
def match(path)
|
131
|
+
m = @matcher.match(path)
|
132
|
+
m if m && m.remaining_path == ''
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Kawaii
|
2
|
+
# Allows handlers to use methods defined in outer contexts or at class scope.
|
3
|
+
# Set {#parent_scope} in constructor.
|
4
|
+
module MethodChain
|
5
|
+
attr_writer :parent_scope
|
6
|
+
|
7
|
+
def method_missing(meth, *args)
|
8
|
+
@parent_scope.send(meth, *args) if @parent_scope.respond_to?(meth)
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to?(method_name, include_private = false)
|
12
|
+
super || @parent_scope.respond_to?(method_name, include_private)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|