kawaii-core 0.1.0
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 +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
|