tanuki 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/README.rdoc +5 -4
  2. data/app/tanuki/controller/{link.thtml → controller.link.thtml} +0 -0
  3. data/app/tanuki/controller/controller.page.thtml +14 -0
  4. data/app/tanuki/controller/controller.rb +1 -2
  5. data/app/tanuki/controller/controller.title.ttxt +1 -0
  6. data/app/tanuki/controller/controller.view.thtml +3 -0
  7. data/app/tanuki/fetcher/sequel/sequel.rb +34 -0
  8. data/app/tanuki/manager/controller/controller.rb +1 -1
  9. data/app/tanuki/manager/page/page.rb +1 -1
  10. data/app/tanuki/meta_model/{manager.ttxt → meta_model.manager.ttxt} +0 -0
  11. data/app/tanuki/meta_model/{manager_base.ttxt → meta_model.manager_base.ttxt} +0 -0
  12. data/app/tanuki/meta_model/{model.ttxt → meta_model.model.ttxt} +0 -0
  13. data/app/tanuki/meta_model/{model_base.ttxt → meta_model.model_base.ttxt} +0 -0
  14. data/app/tanuki/meta_model/meta_model.rb +1 -2
  15. data/app/tanuki/model/controller/controller.rb +1 -1
  16. data/app/tanuki/model/page/page.rb +1 -1
  17. data/app/tanuki/page/missing/{default.thtml → missing.page.thtml} +1 -1
  18. data/app/tanuki/page/missing/missing.rb +3 -2
  19. data/app/user/page/home/home.rb +2 -0
  20. data/app/user/page/home/home.title.thtml +1 -0
  21. data/app/user/page/home/home.view.css +88 -0
  22. data/app/user/page/home/home.view.thtml +22 -0
  23. data/bin/tanuki +2 -1
  24. data/config/common.rb +1 -0
  25. data/config/common_application.rb +3 -6
  26. data/config/development_application.rb +0 -3
  27. data/lib/tanuki.rb +8 -7
  28. data/lib/tanuki/application.rb +108 -81
  29. data/lib/tanuki/argument.rb +10 -5
  30. data/lib/tanuki/argument/integer_range.rb +4 -2
  31. data/lib/tanuki/{behavior/object_behavior.rb → base_behavior.rb} +21 -4
  32. data/lib/tanuki/configurator.rb +20 -8
  33. data/lib/tanuki/const.rb +32 -0
  34. data/lib/tanuki/context.rb +18 -7
  35. data/lib/tanuki/controller.rb +517 -0
  36. data/lib/tanuki/css_compressor.rb +50 -0
  37. data/lib/tanuki/extensions/module.rb +21 -5
  38. data/lib/tanuki/extensions/rack/frozen_route.rb +35 -0
  39. data/lib/tanuki/extensions/rack/static_dir.rb +1 -1
  40. data/lib/tanuki/extensions/sequel/model.rb +7 -0
  41. data/lib/tanuki/i18n.rb +8 -6
  42. data/lib/tanuki/loader.rb +166 -33
  43. data/lib/tanuki/meta_model.rb +176 -0
  44. data/lib/tanuki/{behavior/model_behavior.rb → model_behavior.rb} +7 -3
  45. data/lib/tanuki/model_generator.rb +49 -29
  46. data/lib/tanuki/template_compiler.rb +72 -41
  47. data/lib/tanuki/utility.rb +11 -4
  48. data/lib/tanuki/utility/create.rb +52 -11
  49. data/lib/tanuki/utility/generate.rb +16 -10
  50. data/lib/tanuki/utility/version.rb +1 -1
  51. data/lib/tanuki/version.rb +7 -2
  52. metadata +50 -66
  53. data/app/tanuki/controller/default.thtml +0 -5
  54. data/app/tanuki/controller/index.thtml +0 -1
  55. data/app/user/page/index/default.thtml +0 -121
  56. data/app/user/page/index/index.rb +0 -2
  57. data/config/test_application.rb +0 -2
  58. data/lib/tanuki/behavior/controller_behavior.rb +0 -366
  59. data/lib/tanuki/behavior/meta_model_behavior.rb +0 -160
  60. data/lib/tanuki/extensions/rack/builder.rb +0 -26
  61. data/lib/tanuki/extensions/rack/server.rb +0 -18
  62. data/lib/tanuki/launcher.rb +0 -21
  63. data/lib/tanuki/utility/server.rb +0 -23
@@ -0,0 +1,50 @@
1
+ module Tanuki
2
+
3
+ # Tanuki::CssCompressor takes a CSS source and makes it shorter,
4
+ # trying not to break the semantics.
5
+ class CssCompressor
6
+
7
+ class << self
8
+
9
+ # Compresses CSS source in +css+.
10
+ def compress(css)
11
+ @css = css
12
+ compress_structure
13
+ compress_colors
14
+ compress_dimensions
15
+ @css
16
+ end
17
+
18
+ private
19
+
20
+ # Compresses the CSS structure in +@css+.
21
+ def compress_structure
22
+ @css.gsub!(%r{/\*.*\*/}, '')
23
+ @css.strip!.gsub!(/\s*(\s|;|:|}|{|\)|\(|,)\s*/, '\1')
24
+ @css.gsub!(/[;]+/, ';')
25
+ @css.gsub!(';}', '}')
26
+ end
27
+
28
+ # Compresses CSS color values in +@css+.
29
+ def compress_colors
30
+ @css.gsub! /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/ do
31
+ r = $~[1].to_i.to_s(16).rjust(2, '0')
32
+ g = $~[2].to_i.to_s(16).rjust(2, '0')
33
+ b = $~[3].to_i.to_s(16).rjust(2, '0')
34
+ "##{r}#{g}#{b}"
35
+ end
36
+ @css.gsub!(/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/, '#\1\2\3')
37
+ end
38
+
39
+ # Compresses CSS dimension values in +@css+.
40
+ def compress_dimensions
41
+ @css.gsub!(/(?<=[\s:])0(?:%|cm|em|ex|in|mm|pc|pt|px)/, '0')
42
+ @css.gsub!(/(?<=[\s:])([0-9]+[a-z]*)\s+\1\s+\1\s+\1/, '\1')
43
+ @css.gsub!(/(?<=[\s:])(\d+[a-z]*)\s+(\d+[a-z]*)\s+\1\s+\2/, '\1 \2')
44
+ end
45
+
46
+ end
47
+
48
+ end # CssCompressor
49
+
50
+ end # Tanuki
@@ -1,21 +1,37 @@
1
1
  class Module
2
2
 
3
3
  # Runs Tanuki::Loader for every missing constant in any namespace.
4
- def const_missing(sym)
5
- klass = "#{name + '::' if name != 'Object'}#{sym}"
4
+ def const_missing_with_balls(sym)
5
+ klass = "#{name + '::' unless name.nil? || (name == 'Object')}#{sym}"
6
6
  paths = Dir.glob(Tanuki::Loader.combined_class_path(klass))
7
7
  if paths.empty?
8
8
  unless Dir.glob(Tanuki::Loader.combined_class_dir(klass)).empty?
9
- return const_set(sym, Class.new)
9
+ return const_set(sym, Module.new)
10
10
  end
11
11
  else
12
12
  paths.reverse_each {|path| require path }
13
13
  return const_get(sym) if const_defined?(sym)
14
14
  end
15
- raise NameError, "uninitialized constant #{name}::#{sym}"
15
+ const_missing_without_balls(sym)
16
16
  end
17
+ alias_method_chain :const_missing, :balls
17
18
 
18
- # Creates a reader +sym+ and a writer +sym=+ for the instance variable @_sym.
19
+ # Sets the named constant to the given object, returning that object.
20
+ # Creates new constants and modules recursively if no constant with the
21
+ # given name and nesting previously existed.
22
+ def const_set_recursive(sym, obj)
23
+ sym.to_s.split('::').inject(self) do |klass, const|
24
+ if const_defined? const.to_sym
25
+ const = const_get(const)
26
+ else
27
+ const = const_set(const, Module.new)
28
+ end
29
+ const
30
+ end
31
+ end
32
+
33
+ # Creates a reader +sym+ and a writer +sym=+
34
+ # for the instance variable @_sym.
19
35
  def internal_attr_accessor(*syms)
20
36
  internal_attr_reader(*syms)
21
37
  internal_attr_writer(*syms)
@@ -0,0 +1,35 @@
1
+ require 'digest'
2
+
3
+ module Rack
4
+ class FrozenRoute
5
+
6
+ # Creates a new +route+ (defined by a regular expression)
7
+ # that yields the same +body+ throughout the whole life of the
8
+ # application.
9
+ def initialize(app, route, content_type, body)
10
+ @app = app
11
+ @route = route
12
+ @headers = {
13
+ 'Content-Type' => content_type,
14
+ 'ETag' => Digest::MD5.hexdigest(body)
15
+ }
16
+ @body = body.freeze
17
+ end
18
+
19
+ # Returns the defined +body+ if the route is matched.
20
+ def call(env)
21
+ if env['PATH_INFO'] =~ @route
22
+ if @headers['ETag'] == env['HTTP_IF_NONE_MATCH']
23
+ status = 304
24
+ body = []
25
+ else
26
+ status = 200
27
+ body = @body
28
+ end
29
+ return [status, @headers, body]
30
+ end
31
+ @app.call(env)
32
+ end
33
+
34
+ end # FrozenRoute
35
+ end # Rack
@@ -14,5 +14,5 @@ module Rack
14
14
  @app.call(env)
15
15
  end
16
16
 
17
- end #
17
+ end # StaticDir
18
18
  end # Rack
@@ -0,0 +1,7 @@
1
+ class Sequel::Model
2
+ include Tanuki::BaseBehavior
3
+
4
+ def self.fetcher(controller_class)
5
+ @_tanuki_fetcher ||= Tanuki::Fetcher::Sequel.new(self, controller_class)
6
+ end
7
+ end
@@ -1,9 +1,7 @@
1
1
  module Tanuki
2
2
 
3
3
  # Tanuki::I18n is a drop-in controller for localizable applications.
4
- class I18n
5
-
6
- include ControllerBehavior
4
+ class I18n < Controller
7
5
 
8
6
  # Adds language routes of root controller class when invoked.
9
7
  def configure
@@ -15,12 +13,16 @@ module Tanuki
15
13
  # Returns default route according to default language.
16
14
  def default_route
17
15
  raise 'default language is not configured' unless @_ctx.language
18
- {:route => @_ctx.language, :args => {}, :redirect => @_ctx.i18n_redirect}
16
+ {
17
+ :route => @_ctx.language,
18
+ :args => {},
19
+ :redirect => @_ctx.i18n_redirect
20
+ }
19
21
  end
20
22
 
21
23
  # Calls default view of visual child.
22
- def default_view
23
- @_visual_child.default_view
24
+ def view
25
+ @_visual_child.view
24
26
  end
25
27
 
26
28
  # Adds child controller language to its context.
@@ -1,6 +1,9 @@
1
+ require 'stringio'
2
+
1
3
  module Tanuki
2
4
 
3
- # Tanuki::Loader deals with framework paths resolving, and object and template loading.
5
+ # Tanuki::Loader deals with framework paths resolving,
6
+ # and object and template loading.
4
7
  class Loader
5
8
 
6
9
  class << self
@@ -8,27 +11,43 @@ module Tanuki
8
11
  # Returns the path to a source file in +root+ containing class +klass+.
9
12
  def class_path(klass, root)
10
13
  path = const_to_path(klass, root)
11
- File.join(path, path.match("#{File::SEPARATOR}([^#{File::SEPARATOR}]*)$")[1] << '.rb')
14
+ File.join(path, path.match(%r{/([^/]*)$})[1] << '.rb')
12
15
  end
13
16
 
14
17
  # Returns the path to a source file containing class +klass+.
15
18
  # Seatches across all common roots.
16
19
  def combined_class_path(klass)
17
- class_path(klass, @app_root ||= combined_app_root)
20
+ class_path(klass, @app_root ||= combined_app_root_glob)
18
21
  end
19
22
 
20
23
  # Returns the path to a directory containing class +klass+.
21
24
  # Seatches across all common roots.
22
25
  def combined_class_dir(klass)
23
- const_to_path(klass, @app_root ||= combined_app_root)
26
+ const_to_path(klass, @app_root ||= combined_app_root_glob)
27
+ end
28
+
29
+ # Returns an array with all common roots.
30
+ def combined_app_root(include_gen_root=true)
31
+ local_app_root = File.expand_path('../../../app', __FILE__)
32
+ app_root = [@ctx_app_root ||= @context.app_root]
33
+ app_root << @context.gen_root if include_gen_root
34
+ app_root << local_app_root if local_app_root != @ctx_app_root
35
+ app_root
24
36
  end
25
37
 
26
38
  # Returns a glob pattern of all common roots.
27
- def combined_app_root
28
- local_app_root = File.expand_path(File.join('..', '..', '..', 'app'), __FILE__)
29
- app_root = "{#{context_app_root = @context.app_root},#{@context.gen_root}"
30
- app_root << ",#{local_app_root}" if local_app_root != context_app_root
31
- app_root << '}'
39
+ def combined_app_root_glob(include_gen_root=true)
40
+ "{#{combined_app_root(include_gen_root).join(',')}}"
41
+ end
42
+
43
+ # Returns a regexp pattern of all common roots.
44
+ def combined_app_root_regexp(include_gen_root=true)
45
+ regexp_root = '^('
46
+ regexp_root << combined_app_root(include_gen_root).map do |dir|
47
+ Regexp.escape(dir)
48
+ end.join('|')
49
+ regexp_root << ')/?'
50
+ Regexp.new(regexp_root)
32
51
  end
33
52
 
34
53
  # Assigns a context to Tanuki::Loader.
@@ -37,18 +56,21 @@ module Tanuki
37
56
  @context = ctx
38
57
  end
39
58
 
40
- # Checks if +templates+ contain a compiled template +sym+ for class +klass+.
59
+ # Checks if +templates+ contain a compiled template +sym+
60
+ # for class +klass+.
41
61
  def has_template?(templates, klass, sym)
42
62
  templates.include? "#{klass}##{sym}"
43
63
  end
44
64
 
45
- # Runs template +sym+ with optional +args+ and +block+ from object +obj+.
65
+ # Loads template +sym+.
46
66
  #
47
67
  # Template is recompiled from source on two conditions:
48
- # * template source modification time is older than compiled template modification time,
49
- # * Tanuki::TemplateCompiler source modification time is older than compiled template modification time.
50
- def run_template(templates, obj, sym, *args, &block)
51
- owner, st_path = *template_owner(obj.class, sym)
68
+ # * Template source modification time is older than
69
+ # compiled template modification time,
70
+ # * Tanuki::TemplateCompiler source modification time is older than
71
+ # compiled template modification time.
72
+ def load_template(templates, obj, sym)
73
+ owner, st_path = *resource_owner(obj.class, sym)
52
74
  if st_path
53
75
  ct_path = compiled_template_path(owner, sym)
54
76
  ct_file_exists = File.file?(ct_path)
@@ -56,16 +78,21 @@ module Tanuki
56
78
  st_file = File.new(st_path, 'r:UTF-8')
57
79
 
58
80
  # Find out if template refresh is required
59
- if !ct_file_exists || st_file.mtime > ct_file_mtime || File.mtime(COMPILER_PATH) > ct_file_mtime
60
- no_refresh = compile_template(st_file, ct_path, ct_file_mtime, owner, sym)
81
+ if !ct_file_exists \
82
+ || st_file.mtime > ct_file_mtime \
83
+ || File.mtime(COMPILER_PATH) > ct_file_mtime
84
+ no_refresh = compile_template(
85
+ st_file, ct_path, ct_file_mtime, owner, sym
86
+ )
61
87
  else
62
88
  no_refresh = true
63
89
  end
64
90
 
65
91
  # Load template
66
- method_name = "#{sym}_view".to_sym
92
+ method_name = (sym == 'view' ? sym : "#{sym}_view").to_sym
67
93
  owner.instance_eval do
68
- unless (method_exists = instance_methods(false).include? method_name) && no_refresh
94
+ method_exists = instance_methods(false).include?(method_name)
95
+ unless method_exists && no_refresh
69
96
  remove_method method_name if method_exists
70
97
  load ct_path
71
98
  end
@@ -75,23 +102,121 @@ module Tanuki
75
102
  templates["#{owner}##{sym}"] = nil
76
103
  templates["#{obj.class}##{sym}"] = nil
77
104
 
78
- obj.send(method_name, *args, &block)
105
+ method_name
79
106
  else
80
107
  raise "undefined template `#{sym}' for #{obj.class}"
81
108
  end
82
109
  end
83
110
 
111
+ def load_template_files(ctx, template_signature)
112
+ klass_name, method = *template_signature.split('#')
113
+ klass = klass_name.constantize
114
+ method = method.to_sym
115
+ _, path = *resource_owner(klass, method, JAVASCRIPT_EXT)
116
+ ctx.javascripts[path] = false if path
117
+ ctx.resources[template_signature] = nil
118
+ end
119
+
120
+ # Writes the compiled CSS file to disk
121
+ # and refreshes with a given +interval+ in seconds.
122
+ def refresh_css(interval=5)
123
+ return if @next_reload && @next_reload > Time.new
124
+ mode = File::RDWR|File::CREAT
125
+ File.open("#{@context.public_root}/bundle.css", mode) do |f|
126
+ f.flock(File::LOCK_EX) # Avoid race condition
127
+ now = Time.new
128
+ if !@next_reload || @next_reload < now
129
+ @next_reload = now + interval
130
+ @ctx_app_root ||= @context.app_root
131
+ f.rewind
132
+ compile_css(f, true)
133
+ f.flush.truncate(f.pos)
134
+ end # if
135
+ end # open
136
+ end
137
+
138
+ # Prepares the application for production mode. This includes:
139
+ # * precompiling all templates into memory,
140
+ # * and prebuilding static file like the CSS bundle.
141
+ def prepare_for_production
142
+ root_glob = combined_app_root_glob(false)
143
+ root_re = combined_app_root_regexp(false)
144
+
145
+ # Load application modules
146
+ path_re = /#{root_re}(?<path>.*)/
147
+ Dir.glob(File.join(root_glob, "**/*")) do |file|
148
+ if File.directory? file
149
+ file.match(path_re) do |m|
150
+ mod = File.join(file, File.basename(file)) << '.rb'
151
+ if File.file? mod
152
+ require mod
153
+ else
154
+ Object.const_set_recursive(m[:path].camelize, Module.new)
155
+ end
156
+ end # match
157
+ end # if
158
+ end
159
+
160
+ # Load templates
161
+ file_re = /#{root_re}(?<path>.*)\/.*\.(?<template>.*)\./
162
+ Dir.glob(File.join(root_glob, "**/*#{TEMPLATE_EXT}")) do |file|
163
+ file.match(file_re) do |m|
164
+ ios = StringIO.new
165
+ TemplateCompiler.compile_template(
166
+ ios, File.read(file), m[:path].camelize, m[:template], false
167
+ )
168
+ m[:path].camelize.constantize.class_eval(ios.string)
169
+ end # match
170
+ end # glob
171
+
172
+ # Load CSS
173
+ css = CssCompressor.compress(compile_css(StringIO.new).string)
174
+ Application.use(Rack::FrozenRoute, %r{/bundle.css}, 'text/css', css)
175
+ Application.pull_down(Rack::StaticDir)
176
+ end
177
+
178
+ # Runs template +sym+ with optional +args+ and +block+
179
+ # from object +obj+.
180
+ def run_template(templates, obj, sym, *args, &block)
181
+ method_name = load_template(templates, obj, sym)
182
+ obj.send(method_name, *args, &block)
183
+ end
184
+
84
185
  private
85
186
 
86
187
  # Path to Tanuki::TemplateCompiler for internal use.
87
- COMPILER_PATH = File.expand_path(File.join('..', 'template_compiler.rb'), __FILE__)
188
+ COMPILER_PATH = File.expand_path(
189
+ '../template_compiler.rb',
190
+ __FILE__
191
+ ).freeze
88
192
 
89
193
  # Extension glob for template files.
90
- TEMPLATE_EXT = '.t{html,txt}'
194
+ TEMPLATE_EXT = '.t{html,txt}'.freeze
195
+
196
+ # Extension glob for JavaScript files.
197
+ JAVASCRIPT_EXT = '.js'.freeze
198
+
199
+ # Extension glob for CSS files.
200
+ STYLESHEET_EXT = '.css'.freeze
201
+
202
+ # Compiles all application stylesheets into +ios+.
203
+ # Add path headers for each chunk of CSS if +mark_source+ is true.
204
+ def compile_css(ios, mark_source=false)
205
+ header = "/*** %s ***/\n"
206
+ Dir.glob("#{@ctx_app_root}/**/*#{STYLESHEET_EXT}") do |file|
207
+ if File.file? file
208
+ ios << header % file.sub("#{@ctx_app_root}/", '') if mark_source
209
+ ios << File.read(file) << "\n"
210
+ end
211
+ end
212
+ ios
213
+ end
91
214
 
92
- # Compiles template +sym+ from +owner+ class using source in +st_file+ to +ct_path+.
93
- # Compilation is only done if destination file modification time has not changed
94
- # (is equal to +ct_file_mtime+) since file locking was initiated.
215
+ # Compiles template +sym+ from +owner+ class
216
+ # using source in +st_file+ to +ct_path+.
217
+ # Compilation is only done if destination file modification time
218
+ # has not changed (is equal to +ct_file_mtime+)
219
+ # since file locking was initiated.
95
220
  def compile_template(st_file, ct_path, ct_file_mtime, owner, sym)
96
221
  no_refresh = true
97
222
 
@@ -104,7 +229,9 @@ module Tanuki
104
229
  ct_dir = File.dirname(ct_path)
105
230
  FileUtils.mkdir_p(ct_dir) unless File.directory?(ct_dir)
106
231
  File.open(tmp_ct_path = ct_path + '~', 'w:UTF-8') do |ct_file|
107
- TemplateCompiler.compile_template(ct_file, st_file.read, owner, sym)
232
+ TemplateCompiler.compile_template(
233
+ ct_file, st_file.read, owner, sym
234
+ )
108
235
  end
109
236
  FileUtils.mv(tmp_ct_path, ct_path)
110
237
  end
@@ -115,21 +242,27 @@ module Tanuki
115
242
  no_refresh
116
243
  end
117
244
 
118
- # Returns the path to a compiled template file containing template +method_name+ for class +klass+.
245
+ # Returns the path to a compiled template file
246
+ # containing template +method_name+ for class +klass+.
119
247
  def compiled_template_path(klass, method_name)
120
- File.join(const_to_path(klass, @context.gen_root), method_name.to_s << '.tpl.rb')
248
+ path = const_to_path(klass, @context.gen_root)
249
+ "#{path}/#{method_name.to_s << '.tpl.rb'}"
121
250
  end
122
251
 
123
252
  # Transforms a given constant +klass+ to a path with a given +root+.
124
253
  def const_to_path(klass, root)
125
- File.join(root, klass.to_s.split('::').map {|item| item.gsub(/(?!^)([A-Z])/, '_\1') }.join(File::SEPARATOR).downcase)
254
+ path = klass.to_s.split('::').map(&:underscore).join('/').downcase
255
+ "#{root}/#{path}"
126
256
  end
127
257
 
128
- # Finds the direct template +method_name+ owner among ancestors of class +klass+.
129
- def template_owner(klass, method_name)
130
- method_file = method_name.to_s << TEMPLATE_EXT
258
+ # Finds the direct template +method_name+ owner
259
+ # among ancestors of class +klass+.
260
+ def resource_owner(klass, method_name, extension=TEMPLATE_EXT)
131
261
  klass.ancestors.each do |ancestor|
132
- files = Dir.glob(File.join(const_to_path(ancestor, @app_root ||= combined_app_root), method_file))
262
+ path = const_to_path(ancestor, @app_root ||= combined_app_root_glob)
263
+ method_file = ancestor.to_s.split('::')[-1].underscore.downcase
264
+ method_file << '.' << method_name.to_s << extension
265
+ files = Dir["#{path}/#{method_file}"]
133
266
  return ancestor, files[0] unless files.empty?
134
267
  end
135
268
  [nil, nil]