tanuki 0.3.1 → 0.4.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.
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]