kawaii-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
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,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'kawaii'
2
+
3
+ get '/' do
4
+ 'Hello, world'
5
+ end
@@ -0,0 +1,4 @@
1
+ require 'kawaii'
2
+ require_relative 'hello_world'
3
+
4
+ run Kawaii::SingletonApp
@@ -0,0 +1,11 @@
1
+ require 'kawaii'
2
+ require_relative 'modular/first_app'
3
+ require_relative 'modular/second_app'
4
+
5
+ map '/first' do
6
+ run FirstApp
7
+ end
8
+
9
+ map '/second' do
10
+ run SecondApp
11
+ end
@@ -0,0 +1,6 @@
1
+ # FirstApp -- see modular.ru
2
+ class FirstApp < Kawaii::Base
3
+ get '/' do
4
+ 'First app'
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # SecondApp -- see modular.ru
2
+ class SecondApp < Kawaii::Base
3
+ get '/' do
4
+ 'Second app'
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ require 'kawaii'
2
+
3
+ context '/foo' do
4
+ get '/bar' do
5
+ 'Nested routes'
6
+ end
7
+
8
+ get '/' do
9
+ 'Hello'
10
+ end
11
+ end
data/examples/views.ru ADDED
@@ -0,0 +1,10 @@
1
+ require 'kawaii'
2
+
3
+ class Views < Kawaii::Base
4
+ get '/' do
5
+ @title = 'Hello, world'
6
+ render('index.html.erb')
7
+ end
8
+ end
9
+
10
+ run Views
@@ -0,0 +1 @@
1
+ <h1><%= @title %></h1>
@@ -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'
@@ -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,6 @@
1
+ # Extend String class.
2
+ class String
3
+ def camelcase
4
+ gsub(/(?<=_|^)(\w)/) { Regexp.last_match[1].upcase }.gsub(/(?:_)(\w)/, '\1')
5
+ end
6
+ 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