pakyow-core 0.7.2 → 0.8rc1

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