tanuki 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Anatoly Ressin, Dimitry Solovyov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = Tanuki
2
+
3
+ Tanuki is a web application framework inspired by the MVVM pattern with focus
4
+ on DRY code and extensibility. Tanuki tries to keep its looks close to
5
+ idiomatic Ruby, so you would feel at home.
6
+
7
+ == Quick Start
8
+
9
+ Fire up the terminal and type:
10
+
11
+ $ gem install tanuki
12
+ $ tanuki create test
13
+ $ ruby test/main.rb
14
+
15
+ View the result at: http://localhost:3000
16
+
17
+ == Resources
18
+
19
+ {Home Page}[http://bitbucket.org/dimituri/tanuki]
@@ -0,0 +1,3 @@
1
+ class Tanuki_Controller < Tanuki_Object
2
+ include Tanuki::ControllerBehavior
3
+ end
@@ -0,0 +1,5 @@
1
+ % if visual_child
2
+ <%! visual_child.default_view -%>
3
+ % else
4
+ <%! index_view -%>
5
+ % end
@@ -0,0 +1 @@
1
+ <%= self.class %> is under construction
@@ -0,0 +1,7 @@
1
+ % if current?
2
+ <span class="active"><%= self %></span>
3
+ % elsif active?
4
+ <a class="active" href="<%= self.link %>"><%= self %></a>
5
+ % else
6
+ <a href="<%= self.link %>"><%= self %></a>
7
+ % end
@@ -0,0 +1,3 @@
1
+ class Tanuki_Object
2
+ include Tanuki::ObjectBehavior
3
+ end
@@ -0,0 +1,2 @@
1
+ <h1>Oh no!</h1>
2
+ <p>Tanuki broke free at <%= ctx.env['REQUEST_URI'] %>!</p>
@@ -0,0 +1,9 @@
1
+ class Tanuki_Page_Missing < Tanuki_Controller
2
+ def visual_parent
3
+ nil
4
+ end
5
+
6
+ def result_type
7
+ :not_found
8
+ end
9
+ end
@@ -0,0 +1,121 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Welcome to Tanuki</title>
6
+ <style type="text/css" media="screen">
7
+ body {
8
+ margin: 0;
9
+ padding: 0;
10
+ font-family: 'Trebuchet MS', Helvetica, sans-serif;
11
+ font-size: 11pt;
12
+ background-color: #f5f4ef;
13
+ color: #000305;
14
+ }
15
+
16
+ #header {
17
+ margin: 0 auto;
18
+ width: 540px;
19
+ }
20
+
21
+ div.top {
22
+ background-color: #fff;
23
+ margin: 0 auto 20px auto;
24
+ padding: 15px 20px 0 20px;
25
+ width: 540px;
26
+ border-radius: 10px;
27
+ -moz-border-radius: 10px;
28
+ -webkit-border-radius: 10px;
29
+ }
30
+
31
+ h1 {
32
+ margin: 20px 0;
33
+ padding: 0;
34
+ font-size: 22pt;
35
+ }
36
+
37
+ h1 em {
38
+ display: block;
39
+ font-style: normal;
40
+ font-size: 12pt;
41
+ color: #777a7c;
42
+ }
43
+
44
+ h2 {
45
+ margin: 0 0 15px 0;
46
+ padding: 0;
47
+ font-size: 16pt;
48
+ }
49
+
50
+ #next ol {
51
+ margin: 0;
52
+ padding-bottom: 25px;
53
+ }
54
+
55
+ #next li {
56
+ margin: 0;
57
+ padding: 0;
58
+ font-size: 16pt;
59
+ font-weight: bold;
60
+ color: #c74451;
61
+ }
62
+
63
+ #next li span {
64
+ font-size: 11pt;
65
+ font-weight: normal;
66
+ color: #000305;
67
+ }
68
+
69
+ code {
70
+ font-family: 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', Menlo, Monaco, Consolas, 'Courier New', monospace;
71
+ font-size: 10pt;
72
+ background-color: #fffccc;
73
+ padding: 1px 3px;
74
+ }
75
+
76
+ #env div {
77
+ padding-bottom: 25px;
78
+ }
79
+
80
+ #env table {
81
+ margin: 0;
82
+ padding: 0;
83
+ border-collapse: collapse;
84
+ color: #444;
85
+ }
86
+
87
+ #env table th {
88
+ text-align: left;
89
+ }
90
+
91
+ #env table td {
92
+ padding-left: 20px;
93
+ font-size: 10pt;
94
+ }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <div id="header">
99
+ <h1>It Works! <em>You have been granted one Tanuki.</em></h1>
100
+ </div>
101
+ <div class="top" id="next">
102
+ <h2>What to do next</h2>
103
+ <div>
104
+ <p>Here's a few suggestions to get things going:</p>
105
+ <ol>
106
+ <li><span>To use a database, describe its contents in <code>schema/user</code>.</span></li>
107
+ <li><span>To generate models from your schema, type <code>rake tanuki:models</code>.</span></li>
108
+ <li><span>To add or edit controllers, navigate to <code>app/user</code>.</span></li>
109
+ </ol>
110
+ </div>
111
+ </div>
112
+ <div class="top" id="env">
113
+ <h2>Your setup</h2>
114
+ <div><table>
115
+ <tr><th>Ruby</th><td><%= RUBY_VERSION %> (<%= RUBY_RELEASE_DATE %>) [<%= RUBY_PLATFORM %>]</td></tr>
116
+ <tr><th>Rack</th><td><%= Rack.version %> (on <%= ctx.env['SERVER_SOFTWARE'] %>)</td></tr>
117
+ <tr><th>Tanuki</th><td><%= Tanuki::VERSION %></td></tr>
118
+ </table></div>
119
+ </div>
120
+ </body>
121
+ </html>
@@ -0,0 +1,2 @@
1
+ class User_Page_Index < Tanuki_Controller
2
+ end
data/bin/tanuki ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ module Tanuki
3
+ class << self
4
+ def create(name)
5
+ require 'fileutils'
6
+ version
7
+ puts "\n creating #{name = name.downcase}"
8
+ FileUtils.mkdir name
9
+ puts " copying app/tanuki"
10
+ FileUtils.mkdir_p File.join(name, 'app', 'tanuki')
11
+ FileUtils.cp_r File.expand_path(File.join('..', '..', 'app', 'tanuki'), __FILE__), File.join(name, 'app')
12
+ puts " copying app/user"
13
+ FileUtils.mkdir_p File.join(name, 'app', 'user')
14
+ FileUtils.cp_r File.expand_path(File.join('..', '..', 'app', 'user'), __FILE__), File.join(name, 'app')
15
+ puts " creating cache"
16
+ FileUtils.mkdir(File.join(name, 'cache'))
17
+ puts " copying schema"
18
+ FileUtils.mkdir(File.join(name, 'schema'))
19
+ FileUtils.cp_r File.expand_path(File.join('..', '..', 'schema'), __FILE__), name
20
+ puts " writing main"
21
+ File.open(File.join(name, 'main.rb'), 'w') {|file| file.write "require 'tanuki'\nTanuki.development_application" }
22
+ end
23
+
24
+ def help
25
+ version
26
+ puts "\nbasic commands:\n"
27
+ {
28
+ 'help' => 'show help (this text)',
29
+ 'create' => 'create a new app with the given name',
30
+ 'version' => 'show framework version'
31
+ }.each_pair {|k, v| puts ' %-7s %s' % [k, v] }
32
+ end
33
+
34
+ def init
35
+ case ARGV[0]
36
+ when /\Ac(r(e(a(te?)?)?)?)?\Z/, '--create', '-c' then create(ARGV[1])
37
+ when /\Ah(e(lp?)?)?\Z/, '--help', '-h', nil then help
38
+ when /\Av(e(r(s(i(on?)?)?)?)?)?\Z/, '--version', '-v' then version
39
+ else create(ARGV[0])
40
+ end
41
+ end
42
+
43
+ def version
44
+ begin
45
+ require 'tanuki/version'
46
+ rescue LoadError
47
+ $:.unshift File.expand_path(File.join('..', '..', 'lib'), __FILE__)
48
+ require 'tanuki/version'
49
+ end
50
+ puts "Tanuki version #{VERSION}"
51
+ end
52
+ end
53
+ end
54
+
55
+ Tanuki.init if File.basename(__FILE__) == File.basename($0)
@@ -0,0 +1,111 @@
1
+ module Tanuki
2
+
3
+ # Tanuki::Application is the starting point for all framework applications.
4
+ # It contains core application functionality like configuration, request handling and view management.
5
+ class Application
6
+
7
+ @context = Loader.context = Context.new
8
+ @rack_middleware = {}
9
+
10
+ class << self
11
+
12
+ # Removes a given middleware from the Rack middleware pipeline
13
+ def discard(middleware)
14
+ @rack_middleware.delete(middleware)
15
+ end
16
+
17
+ # Runs the application with current settings
18
+ def run
19
+ rack_builder = Rack::Builder.new
20
+ @rack_middleware.each {|middleware, params| rack_builder.use(middleware, *params[0], &params[1]) }
21
+ rack_builder.run(rack_app)
22
+ srv = available_server
23
+ puts "A wild Tanuki appears!", "You used #{srv.name.gsub(/.*::/, '')} at #{@context.host}:#{@context.port}."
24
+ srv.run rack_builder.to_app, :Host => @context.host, :Port => @context.port
25
+ end
26
+
27
+ # Sets an option to value in the current context.
28
+ def set(option, value)
29
+ @context.send("#{option}=".to_sym, value)
30
+ end
31
+
32
+ # Adds a given middleware to the Rack middleware pipeline
33
+ def use(middleware, *args, &block)
34
+ @rack_middleware[middleware] = [args, block]
35
+ end
36
+
37
+ # Adds a template visitor block. This block must return a Proc.
38
+ #
39
+ # visitor :escape do
40
+ # s = ''
41
+ # proc {|out| s << CGI.escapeHTML(out.to_s) }
42
+ # end
43
+ # visitor :printf do |format|
44
+ # s = ''
45
+ # proc {|out| s << format % out.to_s }
46
+ # end
47
+ #
48
+ # It can later be used in a template via the visitor syntax.
49
+ #
50
+ # <%_escape escaped_view %>
51
+ # <%_printf('<div>%s</div>') formatted_view %>
52
+ def visitor(sym, &block)
53
+ ObjectBehavior.instance_eval { define_method "#{sym}_visitor".to_sym, &block }
54
+ end
55
+
56
+ private
57
+
58
+ # Returns the first available server from a server list in the current context.
59
+ def available_server
60
+ @context.server.each do |server_name|
61
+ begin
62
+ return Rack::Handler.get(server_name.downcase)
63
+ rescue LoadError
64
+ rescue NameError
65
+ end
66
+ end
67
+ raise "servers #{@context.server.join(', ')} not found"
68
+ end
69
+
70
+ # Returns an array of template outputs for controller ctrl in context ctx.
71
+ def build_body(ctrl, request_ctx)
72
+ arr = []
73
+ Launcher.new(ctrl, request_ctx).each &proc {|out| arr << out.to_s }
74
+ end
75
+
76
+ # Returns a Rack app block for Rack::Builder.
77
+ # This block is passed a request environment and returns and array of three elements:
78
+ # * a response status code,
79
+ # * a hash of headers,
80
+ # * an iterable body object.
81
+ # It is run on each request.
82
+ def rack_app
83
+ ctx = @context
84
+ proc do |env|
85
+ request_ctx = ctx.child
86
+ request_ctx.templates = {}
87
+ if match = env['REQUEST_PATH'].match(/^(.+)(?<!\$)\/$/)
88
+ loc = match[1]
89
+ loc << "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].empty?
90
+ [301, {'Location' => loc, 'Content-Type' => 'text/html; charset=utf-8'}, []]
91
+ else
92
+ request_ctx.env = env
93
+ result = ::Tanuki::ControllerBehavior.dispatch(request_ctx, ctx.i18n ? ::Tanuki::I18n : ctx.root_page,
94
+ Rack::Utils.unescape(env['REQUEST_PATH']).force_encoding('UTF-8'))
95
+ case result[:type]
96
+ when :redirect then
97
+ [302, {'Location' => result[:location], 'Content-Type' => 'text/html; charset=utf-8'}, []]
98
+ when :page then
99
+ [200, {'Content-Type' => 'text/html; charset=utf-8'}, build_body(result[:controller], request_ctx)]
100
+ else
101
+ [404, {'Content-Type' => 'text/html; charset=utf-8'}, build_body(result[:controller], request_ctx)]
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ end # end class << self
108
+
109
+ end # end Application
110
+
111
+ end # end Tanuki
@@ -0,0 +1,27 @@
1
+ module Tanuki
2
+ module Argument
3
+
4
+ # Tanuki::Argument::Base is the base class for argument types.
5
+ class Base
6
+
7
+ attr_reader :default, :value
8
+
9
+ # Initializes the argument with a default value.
10
+ def initialize(default)
11
+ @value = @default = default
12
+ end
13
+
14
+ # Returns a string representation of the argument value.
15
+ def to_s
16
+ @value.to_s
17
+ end
18
+
19
+ # Sets the value with required rules.
20
+ def value=(obj)
21
+ @value = to_value(obj)
22
+ end
23
+
24
+ end # end Base
25
+
26
+ end # end Argument
27
+ end # end Tanuki
@@ -0,0 +1,15 @@
1
+ module Tanuki
2
+ module Argument
3
+
4
+ # Tanuki::Argument::Integer is a class for Integer arguments.
5
+ class Integer < Base
6
+
7
+ # Returns argument value from a string representation.
8
+ def to_value(obj)
9
+ begin Kernel::Integer obj rescue @default end
10
+ end
11
+
12
+ end # end Integer
13
+
14
+ end # end Argument
15
+ end # end Tanuki
@@ -0,0 +1,22 @@
1
+ module Tanuki
2
+ module Argument
3
+
4
+ # Tanuki::Argument::IntegerRange is a class for Integer arguments with a certain value range.
5
+ class IntegerRange < Integer
6
+
7
+ # Initializes the argument with a default value and allowed value range.
8
+ def initialize(range, default=nil)
9
+ super(default ? default : range.first)
10
+ @range = range
11
+ end
12
+
13
+ # Returns argument value from a string representation.
14
+ def to_value(obj)
15
+ i = super(obj)
16
+ @range.include?(i) ? i : @default
17
+ end
18
+
19
+ end # end IntegerRange
20
+
21
+ end # end Argument
22
+ end # end Tanuki
@@ -0,0 +1,15 @@
1
+ module Tanuki
2
+ module Argument
3
+
4
+ # Tanuki::Argument::String is a class for String arguments.
5
+ class String < Base
6
+
7
+ # Returns argument value from a string representation.
8
+ def to_value(obj)
9
+ obj.to_s
10
+ end
11
+
12
+ end # end String
13
+
14
+ end # end Argument
15
+ end # end Tanuki
@@ -0,0 +1,41 @@
1
+ require 'tanuki/argument/base'
2
+ require 'tanuki/argument/integer'
3
+ require 'tanuki/argument/integer_range'
4
+ require 'tanuki/argument/string'
5
+
6
+ module Tanuki
7
+
8
+ # Tanuki::Argument contains basic classes and methods for controller arguments.
9
+ module Argument
10
+
11
+ @assoc = {}
12
+
13
+ class << self
14
+
15
+ # Remove argument association for a given type class.
16
+ def delete(klass)
17
+ @assoc.delete(klass)
18
+ end
19
+
20
+ # Associate a given type class with an argument class.
21
+ def store(klass, arg_class)
22
+ warn "Tanuki::Argument::Base is not an ancestor of `#{arg_class}'" unless arg_class.ancestors.include? Argument::Base
23
+ @assoc[klass] = arg_class
24
+ end
25
+
26
+ alias_method :[], :store
27
+
28
+ # Convert a given type object to an argument object.
29
+ def to_argument(obj, *args)
30
+ if @assoc.include?(klass = obj.class)
31
+ @assoc[klass].new(obj, *args)
32
+ else
33
+ Argument::String.new(obj.to_s)
34
+ end
35
+ end
36
+
37
+ end # end class << self
38
+
39
+ end # end Argument
40
+
41
+ end # end Tanuki
@@ -0,0 +1,93 @@
1
+ module Tanuki
2
+
3
+ # Tanuki::Configurator is a scope for evaluating a Tanuki application configuration block.
4
+ # Use Tanuki::development_application and Tanuki::production_application to create such a block.
5
+ class Configurator
6
+
7
+ class << self
8
+
9
+ private
10
+
11
+ # Invokes Tanuki::Argument.store.
12
+ def argument(klass, arg_class)
13
+ Argument.store(klass, arg_class)
14
+ end
15
+
16
+ # Invokes Tanuki::Application.set.
17
+ def set(option, value)
18
+ Application.set(option, value)
19
+ end
20
+
21
+ # Invokes Tanuki::Application.use.
22
+ def use(middleware, *args, &block)
23
+ Application.use(middleware, *args, &block)
24
+ end
25
+
26
+ # Invokes Tanuki::Application.discard.
27
+ def discard(middleware)
28
+ Application.discard(middleware)
29
+ end
30
+
31
+ # Invokes Tanuki::Application.visitor.
32
+ def visitor(sym, &block)
33
+ Application.visitor(sym, &block)
34
+ end
35
+
36
+ end # end class << self
37
+
38
+ end # end Configurator
39
+
40
+ # Creates default configuration for development environments.
41
+ def self.development_application(&block)
42
+ Application.set :development, true
43
+ Application.instance_eval do
44
+ use Rack::CommonLogger
45
+ use Rack::Lint
46
+ use Rack::Reloader, 0
47
+ use Rack::ShowExceptions
48
+ end
49
+ common_application(&block)
50
+ end
51
+
52
+ # Creates default configuration for production environments.
53
+ def self.production_application(&block)
54
+ Application.set :development, false
55
+ common_application(&block)
56
+ end
57
+
58
+ private
59
+
60
+ # Creates default configuration for common environments.
61
+ def self.common_application(&block)
62
+ Application.instance_eval do
63
+ use Rack::Head
64
+ use Rack::ShowStatus
65
+ set :server, [:thin, :mongrel, :webrick]
66
+ set :host, '0.0.0.0'
67
+ set :port, 3000
68
+ set :root, File.expand_path('..', $0)
69
+ set :app_root, proc { File.join(root, 'app') }
70
+ set :cache_root, proc { File.join(root, 'cache') }
71
+ set :schema_root, proc { File.join(root, 'schema') }
72
+ set :root_page, ::User_Page_Index
73
+ set :missing_page, ::Tanuki_Page_Missing
74
+ set :i18n, false
75
+ set :language, nil
76
+ set :language_fallback, {}
77
+ set :languages, proc { language_fallback.keys }
78
+ set :best_language, proc {|lngs| language_fallback[language].each {|lng| return lng if lngs.include? lng }; nil }
79
+ set :best_translation, proc {|trn| language_fallback[language].each {|lng| return trn[lng] if trn.include? lng }; nil }
80
+ visitor :string do s = ''; proc {|out| s << out.to_s } end
81
+ end
82
+ Argument.instance_eval do
83
+ store Fixnum, Argument::Integer
84
+ store Bignum, Argument::Integer
85
+ store Range, Argument::IntegerRange
86
+ store String, Argument::String
87
+ end
88
+ Application.instance_eval { @context = @context.child }
89
+ Configurator.instance_eval(&block) if block_given?
90
+ Application.run
91
+ end
92
+
93
+ end # end Tanuki
@@ -0,0 +1,36 @@
1
+ module Tanuki
2
+
3
+ # Tanuki::Context is used to create unique environments for each request.
4
+ # Once a context variable has been set, it cannot be redefined.
5
+ # Child contexts are used to override parent context variables without changing them directly.
6
+ # Use Tanuki::Context#child to create new contexts.
7
+ class Context
8
+
9
+ # Create and return child context.
10
+ def child
11
+ Class.new(self.class).new
12
+ end
13
+
14
+ # Kernel#method_missing hook.
15
+ def method_missing(sym, arg=nil)
16
+ match = sym.to_s.match(/^([^=]+)(=)?$/)
17
+ self.class.instance_eval do
18
+ method_sym = match[1].to_sym
19
+ unless instance_methods(false).include? method_sym
20
+ if arg.is_a? Proc
21
+ define_method(method_sym, &arg)
22
+ else
23
+ define_method(method_sym) { arg }
24
+ end
25
+ return arg
26
+ else
27
+ raise "context entry `#{match[1]}' redefined, use Context#child"
28
+ end
29
+ end if match[2]
30
+ warn "#{__FILE__}:#{__LINE__}: warning: undefined context entry `#{sym}' for #{self}"
31
+ super
32
+ end
33
+
34
+ end # end Context
35
+
36
+ end # end Tanuki