pakyow-core 0.7.2 → 0.8rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/pakyow-core/bin/pakyow +7 -6
  2. data/pakyow-core/lib/commands/USAGE +2 -1
  3. data/pakyow-core/lib/commands/console.rb +1 -1
  4. data/pakyow-core/lib/commands/server.rb +1 -1
  5. data/pakyow-core/lib/core/application.rb +129 -404
  6. data/pakyow-core/lib/core/base.rb +16 -4
  7. data/pakyow-core/lib/core/configuration/app.rb +6 -2
  8. data/pakyow-core/lib/core/configuration/server.rb +6 -1
  9. data/pakyow-core/lib/core/fn_context.rb +5 -0
  10. data/pakyow-core/lib/core/helpers.rb +16 -0
  11. data/pakyow-core/lib/core/loader.rb +1 -2
  12. data/pakyow-core/lib/core/log.rb +13 -9
  13. data/pakyow-core/lib/core/middleware/logger.rb +37 -0
  14. data/pakyow-core/lib/core/middleware/not_found.rb +40 -0
  15. data/pakyow-core/lib/core/middleware/presenter.rb +25 -0
  16. data/pakyow-core/lib/core/middleware/reloader.rb +14 -0
  17. data/pakyow-core/lib/core/middleware/router.rb +33 -0
  18. data/pakyow-core/lib/core/middleware/setup.rb +15 -0
  19. data/pakyow-core/lib/core/middleware/static.rb +27 -0
  20. data/pakyow-core/lib/core/presenter_base.rb +4 -0
  21. data/pakyow-core/lib/core/request.rb +43 -15
  22. data/pakyow-core/lib/core/response.rb +6 -0
  23. data/pakyow-core/lib/core/route_lookup.rb +37 -0
  24. data/pakyow-core/lib/core/route_set.rb +260 -0
  25. data/pakyow-core/lib/core/route_template.rb +77 -0
  26. data/pakyow-core/lib/core/route_template_defaults.rb +29 -0
  27. data/pakyow-core/lib/core/router.rb +156 -0
  28. data/pakyow-core/lib/generators/pakyow/app/app_generator.rb +12 -2
  29. data/pakyow-core/lib/generators/pakyow/app/templates/app.rb +12 -0
  30. data/pakyow-core/lib/generators/pakyow/app/templates/config.ru +1 -1
  31. data/pakyow-core/lib/generators/pakyow/app/templates/rakefile +1 -1
  32. data/pakyow-core/lib/generators/pakyow/app/templates/{app/views → views}/main.html +0 -0
  33. data/pakyow-core/lib/generators/pakyow/app/templates/{app/views → views}/pakyow.html +1 -1
  34. data/pakyow-core/lib/utils/string.rb +11 -10
  35. metadata +61 -71
  36. data/pakyow-core/lib/core/logger.rb +0 -33
  37. data/pakyow-core/lib/core/reloader.rb +0 -12
  38. data/pakyow-core/lib/core/route_store.rb +0 -220
  39. data/pakyow-core/lib/core/static.rb +0 -25
  40. data/pakyow-core/lib/generators/pakyow/app/templates/app/lib/application_controller.rb +0 -6
  41. data/pakyow-core/lib/generators/pakyow/app/templates/config/application.rb +0 -18
  42. data/pakyow-core/lib/generators/pakyow/app/templates/logs/requests.log +0 -0
@@ -0,0 +1,260 @@
1
+ module Pakyow
2
+ class RouteSet
3
+ def initialize
4
+ @routes = {:get => [], :post => [], :put => [], :delete => []}
5
+
6
+ @routes_by_name = {}
7
+ @grouped_routes_by_name = {}
8
+
9
+ @fns = {}
10
+ @groups = {}
11
+
12
+ @templates = {}
13
+
14
+ @handlers = []
15
+
16
+ @scope = {:name => nil, :path => '/', :hooks => {:before => [], :after => []}}
17
+ end
18
+
19
+ # Creates or retreives a named route function. When retrieving,
20
+ #
21
+ def fn(name, &block)
22
+ @fns[name] = block and return if block
23
+
24
+ #TODO rewrite to not return array
25
+ [@fns[name]]
26
+ end
27
+
28
+ def default(*args, &block)
29
+ self.register_route(:get, '/', *args, &block)
30
+ end
31
+
32
+ def get(*args, &block)
33
+ self.register_route(:get, *args, &block)
34
+ end
35
+
36
+ def put(*args, &block)
37
+ self.register_route(:put, *args, &block)
38
+ end
39
+
40
+ def post(*args, &block)
41
+ self.register_route(:post, *args, &block)
42
+ end
43
+
44
+ def delete(*args, &block)
45
+ self.register_route(:delete, *args, &block)
46
+ end
47
+
48
+ # Returns a lambda that routes request to a controller/action.
49
+ #TODO move to RouteHelpers
50
+ def call(controller, action)
51
+ lambda {
52
+ controller = Object.const_get(controller)
53
+ action ||= Configuration::Base.app.default_action
54
+
55
+ instance = controller.new
56
+ request.controller = instance
57
+ request.action = action
58
+
59
+ instance.send(action)
60
+ }
61
+ end
62
+
63
+ # Creates a handler.
64
+ #
65
+ def handler(name, *args, &block)
66
+ code, fn = args
67
+
68
+ fn = code and code = nil if code.is_a?(Proc)
69
+ fn = block if block_given?
70
+
71
+ @handlers << [name, code, [fn]]
72
+ end
73
+
74
+ def group(name, *args, &block)
75
+ # deep clone existing hooks to reset at the close of this group
76
+ original_hooks = Marshal.load(Marshal.dump(@scope[:hooks]))
77
+
78
+ @scope[:hooks] = self.merge_hooks(@scope[:hooks], args[0]) if @scope[:hooks] && args[0]
79
+
80
+ @scope[:name] = name
81
+ @groups[name] = []
82
+ @grouped_routes_by_name[name] = {}
83
+
84
+ self.instance_exec(&block)
85
+ @scope[:name] = nil
86
+
87
+ @scope[:hooks] = original_hooks
88
+ end
89
+
90
+ def namespace(path, *args, &block)
91
+ name, hooks = args
92
+ hooks = name if name.is_a?(Hash)
93
+
94
+ original_path = @scope[:path]
95
+ @scope[:path] = File.join(@scope[:path], path)
96
+
97
+ self.group(name, hooks || {}, &block)
98
+ @scope[:path] = original_path
99
+ end
100
+
101
+ def template(name, &block)
102
+ @templates[name] = block
103
+ end
104
+
105
+ def expand(t_name, g_name, path = nil, data = nil, &block)
106
+ data = path and path = nil if path.is_a?(Hash)
107
+
108
+ # evaluate block in context of some class that implements
109
+ # method_missing to store map of functions
110
+ # (e.g. index, show)
111
+ t = RouteTemplate.new(block, g_name, path, self)
112
+
113
+ # evaluate template in same context, where func looks up funcs
114
+ # from map and extends get (and others) to add proper names
115
+ t.expand(@templates[t_name], data)
116
+ end
117
+
118
+ # Returns a route tuple:
119
+ # [regex, vars, name, fns, path]
120
+ #
121
+ def match(path, method)
122
+ path = StringUtils.normalize_path(path)
123
+
124
+ @routes[method.to_sym].each{|r|
125
+ case r[0]
126
+ when Regexp
127
+ if data = r[0].match(path)
128
+ return r, data
129
+ end
130
+ when String
131
+ if r[0] == path
132
+ return r, nil
133
+ end
134
+ end
135
+ }
136
+
137
+ nil
138
+ end
139
+
140
+ def handle(name_or_code)
141
+ @handlers.each{ |h|
142
+ return h if h[0] == name_or_code || h[1] == name_or_code
143
+ }
144
+
145
+ nil
146
+ end
147
+
148
+ # Name based route lookup
149
+ def route(name, group = nil)
150
+ return @routes_by_name[name] unless group
151
+
152
+ if grouped_routes = @grouped_routes_by_name[group]
153
+ grouped_routes[name]
154
+ end
155
+ end
156
+
157
+ protected
158
+
159
+ def merge_hooks(h1, h2)
160
+ # normalize
161
+ h1[:before] ||= []
162
+ h1[:after] ||= []
163
+ h1[:around] ||= []
164
+ h2[:before] ||= []
165
+ h2[:after] ||= []
166
+ h2[:around] ||= []
167
+
168
+ # merge
169
+ h1[:before].concat(h2[:before])
170
+ h1[:after].concat(h2[:after])
171
+ h1[:around].concat(h2[:around])
172
+ h1
173
+ end
174
+
175
+ def register_route(method, path, *args, &block)
176
+ name, fns, hooks = self.parse_route_args(args)
177
+
178
+ fns ||= []
179
+ # add passed block to fns
180
+ fns << block if block_given?
181
+
182
+ hooks = self.merge_hooks(hooks || {}, @scope[:hooks])
183
+
184
+ # build the final list of fns
185
+ fns = self.build_fns(fns, hooks)
186
+
187
+ # prepend scope path if we're in a scope
188
+ path = File.join(@scope[:path], path)
189
+ path = StringUtils.normalize_path(path)
190
+
191
+ # get regex and vars for path
192
+ regex, vars = self.build_route_matcher(path)
193
+
194
+ # create the route tuple
195
+ route = [regex, vars, name, fns, path]
196
+
197
+ @routes[method] << route
198
+ @routes_by_name[name] = route
199
+
200
+ # store group references if we're in a scope
201
+ return unless group = @scope[:name]
202
+ @groups[group] << route
203
+ @grouped_routes_by_name[group][name] = route
204
+ end
205
+
206
+ def parse_route_args(args)
207
+ ret = []
208
+ args.each { |arg|
209
+ if arg.is_a?(Hash) # we have hooks
210
+ ret[2] = arg
211
+ elsif arg.is_a?(Array) # we have fns
212
+ ret[1] = arg
213
+ elsif arg.is_a?(Proc) # we have a fn
214
+ ret[1] = [arg]
215
+ elsif !arg.nil? # we have a name
216
+ ret[0] = arg
217
+ end
218
+ }
219
+ ret
220
+ end
221
+
222
+ def build_fns(main_fns, hooks)
223
+ fns = []
224
+ fns.concat(hooks[:around]) if hooks && hooks[:around]
225
+ fns.concat(hooks[:before]) if hooks && hooks[:before]
226
+ fns.concat(main_fns) if main_fns
227
+ fns.concat(hooks[:after]) if hooks && hooks[:after]
228
+ fns.concat(hooks[:around]) if hooks && hooks[:around]
229
+ fns
230
+ end
231
+
232
+ def build_route_matcher(path)
233
+ return path, [] if path.is_a?(Regexp)
234
+
235
+ # check for vars
236
+ return path, [] unless path[0,1] == ':' || path.index('/:')
237
+
238
+ # we have vars
239
+ vars = []
240
+ position_counter = 1
241
+ regex_route = path
242
+ route_segments = path.split('/')
243
+ route_segments.each_with_index { |segment, i|
244
+ if segment.include?(':')
245
+ vars << { :position => position_counter, :var => segment.gsub(':', '').to_sym }
246
+ if i == route_segments.length-1 then
247
+ regex_route = regex_route.sub(segment, '((\w|[-.~:@!$\'\(\)\*\+,;])*)')
248
+ position_counter += 2
249
+ else
250
+ regex_route = regex_route.sub(segment, '((\w|[-.~:@!$\'\(\)\*\+,;])*)')
251
+ position_counter += 2
252
+ end
253
+ end
254
+ }
255
+ reg = Regexp.new("^#{regex_route}$")
256
+ return reg, vars
257
+ end
258
+ end
259
+ end
260
+
@@ -0,0 +1,77 @@
1
+ #TODO rename router to set and .func to .fn
2
+
3
+ module Pakyow
4
+ class RouteTemplate
5
+ attr_accessor :path
6
+
7
+ def initialize(block, g_name, path, router)
8
+ @fns = {}
9
+ @g_name = g_name
10
+ @path = path
11
+ @router = router
12
+
13
+ self.instance_exec(&block)
14
+ end
15
+
16
+ def action(method, *args, &block)
17
+ fns = block_given? ? [block] : args[0]
18
+ @fns[method] = fns
19
+ end
20
+
21
+ def expand(template, data)
22
+ @expanding = true
23
+
24
+ t = self
25
+ if @path
26
+ @router.namespace(@path, @g_name) {
27
+ t.instance_exec(data, &template)
28
+ }
29
+ else
30
+ @router.group(@g_name) {
31
+ t.instance_exec(data, &template)
32
+ }
33
+ end
34
+ end
35
+
36
+ def fn(name)
37
+ @expanding ? @fns[name] : @router.func(name)
38
+ end
39
+
40
+ def call(controller, action)
41
+ @router.call(controller, action)
42
+ end
43
+
44
+ def get(*args, &block)
45
+ @router.get(*args, &block)
46
+ end
47
+
48
+ def put(*args, &block)
49
+ @router.put(*args, &block)
50
+ end
51
+
52
+ def post(*args, &block)
53
+ @router.post(*args, &block)
54
+ end
55
+
56
+ def delete(*args, &block)
57
+ @router.delete(*args, &block)
58
+ end
59
+
60
+ #TODO best name?
61
+ def map_actions(controller, actions)
62
+ actions.each { |a|
63
+ self.action(a, self.call(controller, a))
64
+ }
65
+ end
66
+
67
+ #TODO best name?
68
+ def map_restful_actions(controller)
69
+ self.map_actions(controller, self.restful_actions)
70
+ end
71
+
72
+ def restful_actions
73
+ [:index, :show, :new, :create, :edit, :update, :delete]
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,29 @@
1
+ module Pakyow
2
+ class RouteTemplateDefaults
3
+ def self.register
4
+ Pakyow::Router.instance.set(:default) {
5
+ template(:restful) {
6
+ get '/', :index, fn(:index)
7
+
8
+ # special case for show (view path is overridden)
9
+ if show_fns = fn(:show)
10
+ show_fns = [show_fns] unless show_fns.is_a?(Array)
11
+ get '/:id', :show, show_fns.unshift(
12
+ lambda {
13
+ presenter.view_path = File.join(self.path, 'show') if Configuration::Base.app.presenter
14
+ }
15
+ )
16
+ end
17
+
18
+ get '/new', :new, fn(:new)
19
+ post '/', :create, fn(:create)
20
+
21
+ get '/:id/edit', :edit, fn(:edit)
22
+ put '/:id', :update, fn(:update)
23
+
24
+ delete '/:id', :delete, fn(:delete)
25
+ }
26
+ }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,156 @@
1
+ require 'singleton'
2
+
3
+ module Pakyow
4
+ # A singleton that manages route sets.
5
+ #
6
+ class Router
7
+ include Singleton
8
+
9
+ def initialize
10
+ end
11
+
12
+ def reset
13
+ @sets = {}
14
+ RouteTemplateDefaults.register
15
+ self
16
+ end
17
+
18
+ # Creates a new set (or appends to a set if it exists).
19
+ #
20
+ def set(name, &block)
21
+ @sets[name] ||= RouteSet.new
22
+ @sets[name].instance_exec(&block)
23
+ end
24
+
25
+ # Iterates through route sets and returns the first matching route.
26
+ #
27
+ def route(name, group = nil)
28
+ @sets.each { |set|
29
+ if r = set[1].route(name, group)
30
+ return r
31
+ end
32
+ }
33
+
34
+ nil
35
+ end
36
+
37
+ # Performs the initial routing for a request.
38
+ #
39
+ def route!(request)
40
+ self.trampoline(self.match(request))
41
+ end
42
+
43
+ # Reroutes a request.
44
+ #
45
+ def reroute!(request)
46
+ throw :fns, self.match(request)
47
+ end
48
+
49
+ # Finds and invokes a handler by name or by status code.
50
+ #
51
+ def handle!(name_or_code, from_logic = false)
52
+ @sets.each { |set|
53
+ if h = set[1].handle(name_or_code)
54
+ Pakyow.app.response.status = h[0]
55
+ from_logic ? throw(:fns, h[2]) : self.trampoline(h[2])
56
+ break
57
+ end
58
+ }
59
+ end
60
+
61
+ def routed?
62
+ @routed
63
+ end
64
+
65
+ # Looks up and populates a path with data
66
+ #
67
+ def path(name, data = nil)
68
+ RouteLookup.new.path(name, data)
69
+ end
70
+
71
+ # Looks up a route grouping
72
+ #
73
+ def group(name)
74
+ RouteLookup.new.group(name)
75
+ end
76
+
77
+ protected
78
+
79
+ # Calls a list of route functions in order (each in a separate context).
80
+ #
81
+ def call_fns(fns)
82
+ fns.each {|fn| self.context.instance_exec(&fn)}
83
+ end
84
+
85
+ # Creates a context in which to evaluate a route function.
86
+ #
87
+ def context
88
+ FnContext.new
89
+ end
90
+
91
+ # Finds the first matching route for the request path/method and
92
+ # returns the list of route functions for that route.
93
+ #
94
+ def match(request)
95
+ path = StringUtils.normalize_path(request.working_path)
96
+ method = request.working_method
97
+
98
+ @routed = false
99
+
100
+ match, data = nil
101
+ @sets.each { |set|
102
+ match, data = set[1].match(path, method)
103
+ break if match
104
+ }
105
+
106
+ fns = []
107
+ if match
108
+ fns = match[3]
109
+
110
+ # handle route params
111
+ #TODO where to do this?
112
+ request.params.merge!(HashUtils.strhash(self.data_from_path(path, data, match[1])))
113
+
114
+ #TODO where to do this?
115
+ request.route_path = match[4]
116
+ end
117
+
118
+ fns
119
+ end
120
+
121
+ # Calls route functions and catches new functions as
122
+ # they're thrown (e.g. by reroute).
123
+ #
124
+ def trampoline(fns)
125
+ until fns.empty?
126
+ fns = catch(:fns) {
127
+ self.call_fns(fns)
128
+
129
+ # Getting here means that call() returned normally (not via a throw)
130
+ :fall_through
131
+ } # end :fns catch block
132
+
133
+ # If reroute! or invoke_handler! was called in the block, block will have a new value (nil or block).
134
+ # If neither was called, block will be :fall_through
135
+
136
+ @routed = case fns
137
+ when [] then false
138
+ when :fall_through then fns = [] and true
139
+ end
140
+ end
141
+ end
142
+
143
+ # Extracts the data from a path.
144
+ #
145
+ def data_from_path(path, matches, vars)
146
+ data = {}
147
+ return data unless matches
148
+
149
+ vars.each {|v|
150
+ data[v[:var]] = matches[v[:position]]
151
+ }
152
+
153
+ data
154
+ end
155
+ end
156
+ end
@@ -5,7 +5,8 @@ module Pakyow
5
5
  class AppGenerator
6
6
  class << self
7
7
  def start
8
- if ARGV.first == '--help' || ARGV.first == '-h'
8
+ case ARGV.first
9
+ when '--help', '-h', nil
9
10
  puts File.open(File.join(CORE_PATH, 'commands/USAGE-NEW')).read
10
11
  else
11
12
  generator = self.new
@@ -19,7 +20,16 @@ module Pakyow
19
20
  end
20
21
 
21
22
  def build(dest)
22
- FileUtils.cp_r(@src, dest)
23
+ if !File.directory?(dest) || (Dir.entries(dest) - ['.', '..']).empty?
24
+ FileUtils.cp_r(@src, dest)
25
+ else
26
+ ARGV.clear
27
+ print "The folder '#{dest}' is in use. Would you like to populate it anyway? [Yn] "
28
+
29
+ if gets.chomp! == 'Y'
30
+ FileUtils.cp_r(@src, dest)
31
+ end
32
+ end
23
33
  end
24
34
  end
25
35
  end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'pakyow'
3
+
4
+ module PakyowApplication
5
+ class Application < Pakyow::Application
6
+ core do
7
+ default {
8
+ puts 'Pakyow says hello!'
9
+ }
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
- require File.expand_path('../config/application', __FILE__)
1
+ require File.expand_path('app', __FILE__)
2
2
  PakyowApplication::Application.builder.run(PakyowApplication::Application.stage(ENV['RACK_ENV']))
3
3
  run PakyowApplication::Application.builder.to_app
@@ -1,2 +1,2 @@
1
- require File.expand_path('../config/application', __FILE__)
1
+ require File.expand_path('application', __FILE__)
2
2
  PakyowApplication::Application.stage(ENV['ENV'])
@@ -6,7 +6,7 @@
6
6
 
7
7
  <body>
8
8
  <div id="wrapper">
9
- <div id="main"></div>
9
+ <div data-container="main"></div>
10
10
  </div>
11
11
  </body>
12
12
  </html>
@@ -3,16 +3,6 @@ module Pakyow
3
3
  # Utility methods for strings.
4
4
  class StringUtils
5
5
 
6
- # Creates an underscored, lowercase version of a string.
7
- # This was borrowed from another library, probably ActiveSupport.
8
- def self.underscore(string)
9
- string.gsub(/::/, '/').
10
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
11
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
12
- tr("-", "_").
13
- downcase
14
- end
15
-
16
6
  # split . seperated string at the last .
17
7
  def self.split_at_last_dot(s)
18
8
  split_index = s.rindex('.')
@@ -32,6 +22,17 @@ module Pakyow
32
22
  return ret
33
23
  end
34
24
 
25
+ def self.parse_path_from_caller(caller)
26
+ caller.match(/^(.+)(:?:\d+(:?:in `.+')?$)/)[1]
27
+ end
28
+
29
+ def self.normalize_path(path)
30
+ return path if path.is_a?(Regexp)
31
+
32
+ path = path[1, path.length - 1] if path[0, 1] == '/'
33
+ path = path[0, path.length - 1] if path[path.length - 1, 1] == '/'
34
+ path
35
+ end
35
36
 
36
37
  end
37
38
  end