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.
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