orange-core 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/README.markdown +145 -0
  2. data/lib/orange-core.rb +8 -0
  3. data/lib/orange-core/application.rb +132 -0
  4. data/lib/orange-core/assets/css/exceptions.css +50 -0
  5. data/lib/orange-core/assets/js/exceptions.js +44 -0
  6. data/lib/orange-core/carton.rb +178 -0
  7. data/lib/orange-core/core.rb +266 -0
  8. data/lib/orange-core/magick.rb +270 -0
  9. data/lib/orange-core/middleware/base.rb +96 -0
  10. data/lib/orange-core/middleware/database.rb +45 -0
  11. data/lib/orange-core/middleware/four_oh_four.rb +45 -0
  12. data/lib/orange-core/middleware/globals.rb +17 -0
  13. data/lib/orange-core/middleware/loader.rb +13 -0
  14. data/lib/orange-core/middleware/rerouter.rb +53 -0
  15. data/lib/orange-core/middleware/restful_router.rb +99 -0
  16. data/lib/orange-core/middleware/route_context.rb +39 -0
  17. data/lib/orange-core/middleware/route_site.rb +51 -0
  18. data/lib/orange-core/middleware/show_exceptions.rb +80 -0
  19. data/lib/orange-core/middleware/static.rb +67 -0
  20. data/lib/orange-core/middleware/static_file.rb +32 -0
  21. data/lib/orange-core/middleware/template.rb +60 -0
  22. data/lib/orange-core/packet.rb +232 -0
  23. data/lib/orange-core/plugin.rb +172 -0
  24. data/lib/orange-core/resource.rb +96 -0
  25. data/lib/orange-core/resources/mapper.rb +36 -0
  26. data/lib/orange-core/resources/model_resource.rb +228 -0
  27. data/lib/orange-core/resources/not_found.rb +10 -0
  28. data/lib/orange-core/resources/page_parts.rb +68 -0
  29. data/lib/orange-core/resources/parser.rb +113 -0
  30. data/lib/orange-core/resources/routable_resource.rb +16 -0
  31. data/lib/orange-core/resources/scaffold.rb +106 -0
  32. data/lib/orange-core/stack.rb +226 -0
  33. data/lib/orange-core/templates/exceptions.haml +111 -0
  34. data/lib/orange-core/views/default_resource/create.haml +4 -0
  35. data/lib/orange-core/views/default_resource/edit.haml +9 -0
  36. data/lib/orange-core/views/default_resource/list.haml +10 -0
  37. data/lib/orange-core/views/default_resource/show.haml +4 -0
  38. data/lib/orange-core/views/default_resource/table_row.haml +7 -0
  39. data/lib/orange-core/views/not_found/404.haml +2 -0
  40. data/spec/orange-core/application_spec.rb +183 -0
  41. data/spec/orange-core/carton_spec.rb +136 -0
  42. data/spec/orange-core/core_spec.rb +248 -0
  43. data/spec/orange-core/magick_spec.rb +96 -0
  44. data/spec/orange-core/middleware/base_spec.rb +38 -0
  45. data/spec/orange-core/middleware/globals_spec.rb +3 -0
  46. data/spec/orange-core/middleware/rerouter_spec.rb +3 -0
  47. data/spec/orange-core/middleware/restful_router_spec.rb +3 -0
  48. data/spec/orange-core/middleware/route_context_spec.rb +3 -0
  49. data/spec/orange-core/middleware/route_site_spec.rb +3 -0
  50. data/spec/orange-core/middleware/show_exceptions_spec.rb +3 -0
  51. data/spec/orange-core/middleware/static_file_spec.rb +3 -0
  52. data/spec/orange-core/middleware/static_spec.rb +3 -0
  53. data/spec/orange-core/mock/mock_app.rb +16 -0
  54. data/spec/orange-core/mock/mock_carton.rb +43 -0
  55. data/spec/orange-core/mock/mock_core.rb +2 -0
  56. data/spec/orange-core/mock/mock_middleware.rb +25 -0
  57. data/spec/orange-core/mock/mock_mixins.rb +19 -0
  58. data/spec/orange-core/mock/mock_model_resource.rb +47 -0
  59. data/spec/orange-core/mock/mock_pulp.rb +24 -0
  60. data/spec/orange-core/mock/mock_resource.rb +26 -0
  61. data/spec/orange-core/mock/mock_router.rb +10 -0
  62. data/spec/orange-core/orange_spec.rb +19 -0
  63. data/spec/orange-core/packet_spec.rb +203 -0
  64. data/spec/orange-core/resource_spec.rb +96 -0
  65. data/spec/orange-core/resources/mapper_spec.rb +5 -0
  66. data/spec/orange-core/resources/model_resource_spec.rb +246 -0
  67. data/spec/orange-core/resources/parser_spec.rb +5 -0
  68. data/spec/orange-core/resources/routable_resource_spec.rb +5 -0
  69. data/spec/orange-core/spec_helper.rb +53 -0
  70. data/spec/orange-core/stack_spec.rb +232 -0
  71. data/spec/stats.rb +182 -0
  72. metadata +227 -0
@@ -0,0 +1,80 @@
1
+ require 'ostruct'
2
+ require 'rack/request'
3
+ require 'rack/utils'
4
+ require 'orange-core/middleware/base'
5
+
6
+ module Orange::Middleware
7
+ # Rack::ShowExceptions catches all exceptions raised from the app it
8
+ # wraps. It shows a useful backtrace with the sourcefile and
9
+ # clickable context, the whole Rack environment and the request
10
+ # data.
11
+ #
12
+ # Be careful when you use this on public-facing sites as it could
13
+ # reveal information helpful to attackers.
14
+ #
15
+ # Orange::Middleware::ShowExceptions is a slightly modified
16
+ # version of Rack::ShowExceptions
17
+ class ShowExceptions < Base
18
+ CONTEXT = 7
19
+
20
+ def call(env)
21
+ @app.call(env)
22
+ rescue Exception => e
23
+ backtrace = pretty(env, e)
24
+
25
+ [500,
26
+ {"Content-Type" => "text/html",
27
+ "Content-Length" => backtrace.join.size.to_s},
28
+ backtrace]
29
+ end
30
+
31
+ def packet_call(packet)
32
+ backtrace = pretty()
33
+ end
34
+
35
+ def pretty(env, exception)
36
+ req = Rack::Request.new(env)
37
+ path = (req.script_name + req.path_info).squeeze("/")
38
+
39
+ frames = exception.backtrace.map { |line|
40
+ frame = OpenStruct.new
41
+ if line =~ /(.*?):(\d+)(:in `(.*)')?/
42
+ frame.filename = $1
43
+ frame.lineno = $2.to_i
44
+ frame.function = $4
45
+
46
+ begin
47
+ lineno = frame.lineno-1
48
+ lines = ::File.readlines(frame.filename)
49
+ frame.pre_context_lineno = [lineno-CONTEXT, 0].max
50
+ frame.pre_context = lines[frame.pre_context_lineno...lineno]
51
+ frame.context_line = lines[lineno].chomp
52
+ frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
53
+ frame.post_context = lines[lineno+1..frame.post_context_lineno]
54
+ rescue
55
+ end
56
+
57
+ frame
58
+ else
59
+ nil
60
+ end
61
+ }.compact
62
+
63
+ env["rack.errors"].puts "#{exception.class}: #{exception.message}"
64
+ env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l }
65
+ env["rack.errors"].flush
66
+ orange_env = env["orange.env"]
67
+ parse = orange[:parser].haml("exceptions.haml", binding, :template => true)
68
+ [parse]
69
+ end
70
+
71
+ def h(obj) # :nodoc:
72
+ case obj
73
+ when String
74
+ Rack::Utils.escape_html(obj)
75
+ else
76
+ Rack::Utils.escape_html(obj.inspect)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ require 'orange-core/middleware/base'
2
+ require 'orange-core/middleware/static_file'
3
+
4
+ module Orange::Middleware
5
+ # The Orange::Middleware::Static middleware intercepts requests for static files
6
+ # (javascript files, images, stylesheets, etc) based on the url prefixes
7
+ # passed in the options, and serves them using a Rack::File object.
8
+ #
9
+ # This differs from Rack::Static in that it can serve from multiple roots
10
+ # to accommodate both Orange static files and site specific ones.
11
+ # urls and root act the same as they do for Rack::Static. Only :libs option acts
12
+ # specially.
13
+ #
14
+ # Each lib is responsible for responding to static_url and static_dir
15
+ #
16
+ # Examples:
17
+ # use Orange::Middleware::Static :libs => [Orange::Core, AwesomeMod]
18
+ # use Orange::Middleware::Static :libs => [Orange::Core, AwesomeMod],
19
+ # :urls => ["/favicon.ico"]
20
+ #
21
+ # => Example 1 would load a file root for Orange::Core and Awesome Mod
22
+ # Orange::Core static_url is _orange_, and dir is the
23
+ # orange lib/assets folder
24
+ # => Example 2 would also redirect favicon.ico to the assets dir
25
+ #
26
+ #
27
+ # Note that as a general rule, Orange will assume everything static to be in an
28
+ # /assets/ subfolder, therefore, '/assets' will be prepended to the url given
29
+ # by static_url
30
+ # Also note, that since this is the case - setting up a match for general '/assets'
31
+ # could yield unpredictable results
32
+ #
33
+ # a static_url corresponds to the :module => in the add_css and add_js helpers
34
+ class Static < Base
35
+
36
+ def initialize(app, core, options={})
37
+ @lib_urls = {'_orange_' => File.join(core.core_dir, 'assets') }
38
+ Orange.plugins.each{|p| @lib_urls[p.assets_name] = p.assets if p.has_assets?}
39
+
40
+ @app = app
41
+ @core = core
42
+ @urls = options[:urls] || ["/favicon.ico", "/assets/public", "/assets/uploaded"]
43
+ @root = options[:root] || File.join(orange.app_dir, 'assets')
44
+ @file_server = Orange::Middleware::StaticFile.new(@root)
45
+ end
46
+
47
+ def packet_call(packet)
48
+ path = packet.env["PATH_INFO"]
49
+ can_serve_lib = @lib_urls.select { |url, server|
50
+ path.index(File.join('', 'assets', url)) == 0
51
+ }.first
52
+ can_serve = @urls.any?{|url| path.index(url) == 0 }
53
+ if can_serve_lib
54
+ lib_url = can_serve_lib.first
55
+ packet['file.root'] = can_serve_lib.last
56
+ packet['route.path'] = path.split(lib_url, 2).last
57
+ @file_server.call(packet.env)
58
+ elsif can_serve
59
+ packet['route.path'] = path.gsub(/^\/assets/, '')
60
+ @file_server.call(packet.env)
61
+ else
62
+ pass packet
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ require 'rack'
2
+ require 'rack/utils'
3
+
4
+ module Orange::Middleware
5
+ # Rack::File serves files below the +root+ given, according to the
6
+ # path info of the Rack request.
7
+ # Orange::Middleware::StaticFile acts the same as Rack::File, but acts on
8
+ # the orange specific path if available. (So site url would be ignored, etc.)
9
+ #
10
+ # Handlers can detect if bodies are a Rack::File, and use mechanisms
11
+ # like sendfile on the +path+.
12
+
13
+ class StaticFile < Rack::File
14
+ def _call(env)
15
+ @path_info = Rack::Utils.unescape(env['orange.env']["route.path"]) || Rack::Utils.unescape(env["PATH_INFO"])
16
+ @root = env['orange.env']['file.root'] || @root
17
+ return forbidden if @path_info.include? ".."
18
+
19
+ @path = F.join(@root, @path_info)
20
+
21
+ begin
22
+ if F.file?(@path) && F.readable?(@path)
23
+ serving
24
+ else
25
+ raise Errno::EPERM
26
+ end
27
+ rescue SystemCallError
28
+ not_found
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ require 'orange-core/core'
2
+ require 'orange-core/middleware/base'
3
+
4
+ module Orange::Middleware
5
+
6
+ class Template < Base
7
+ def init(*args)
8
+ @core.add_pulp(Orange::Pulp::Template)
9
+ @core.mixin(Orange::Mixins::Template)
10
+
11
+ end
12
+
13
+ def packet_call(packet)
14
+ packet['template.file'] = orange.template_for packet
15
+ status, headers, content = pass packet
16
+ if needs_wrapped?(packet)
17
+ content = wrap(packet, content)
18
+ packet[:content] = content.first
19
+ orange.fire(:wrapped, packet)
20
+ end
21
+ orange.fire(:after_wrap, packet)
22
+ packet.finish
23
+ end
24
+
25
+ def needs_wrapped?(packet)
26
+ return false if packet.request.xhr? && !packet['template.enable'] # don't wrap xhr unless specifically asked to
27
+ packet['template.file'] && !packet['template.disable']
28
+ end
29
+
30
+ def wrap(packet, content = false)
31
+ content = packet.content unless content
32
+ content = content.join
33
+ content = orange[:parser].haml(packet['template.file'], packet, :wrapped_content => content, :template => true) do
34
+ content
35
+ end
36
+ [content]
37
+ end
38
+ end
39
+ end
40
+
41
+ module Orange::Pulp::Template
42
+ def wrap
43
+ packet[:content] = orange[:parser].haml(packet['template.file'], packet, :wrapped_content => packet[:content], :template => true) do
44
+ content
45
+ end
46
+ end
47
+ end
48
+
49
+ module Orange::Mixins::Template
50
+ def template_for(packet)
51
+ template_chooser.call(packet)
52
+ end
53
+ def template_chooser(&block)
54
+ if block_given?
55
+ @template_chooser = Proc.new
56
+ else
57
+ @template_chooser ||= Proc.new {|packet| false}
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,232 @@
1
+ module Orange
2
+ # Orange::Packet is a wrapper for Rack's basic env variable.
3
+ # It acts somewhat like Rack::Request, except with more functionality.
4
+ # For each request a unique Packet is generated, and this packet is
5
+ # used to by middleware to create the response.
6
+ #
7
+ # All orange enhanced middleware has a packet_call method that
8
+ # automatically turns the generic rack call(env) into a call
9
+ # that has a packet instead, so all functions for the packet should
10
+ # be available for the request.
11
+ #
12
+ # Pulps are modules that are mixed into the Packet, allowing
13
+ # additional functionality to be used by the packet.
14
+ #
15
+ # By default, haml files are parsed in the context of their
16
+ # packet. This means all of instance variables and functions should
17
+ # be available to the haml parser.
18
+ class Packet
19
+ # By default, header will be content-type
20
+ DEFAULT_HEADERS = {"Content-Type" => 'text/html'} unless defined?(DEFAULT_HEADERS)
21
+
22
+ # We override the instantiation to only create one packet per env
23
+ # @param [Orange::Core] orange a pointer to the orange core
24
+ # @param [Hash] env a standard Rack hash
25
+ def self.new(orange, env)
26
+ return env['orange.packet'] if env['orange.packet']
27
+ super(orange, env)
28
+ end
29
+
30
+ # Allows tying in to the method_missing method without redefining it
31
+ # elsewhere. This lets dynamic methods be defined on the packet.
32
+ # Regexes are defined to match method names. #method_missing will
33
+ # loop through and try to find a match, executing the proc defined in
34
+ # the block.
35
+ # @param [Regexp] regex the regex to match
36
+ # @yield [Orange::Packet, MatchData, args] the block to execute if matched
37
+ # (passed instance, match data and args)
38
+ def self.meta_methods(regex, &block)
39
+ return unless block_given?
40
+ proc = block
41
+ @@matchers ||= {}
42
+ @@matchers[regex] = proc
43
+ end
44
+
45
+ # Allows access to the matchers added via the #meta_methods method
46
+ # @return [Hash] the matchers hash
47
+ def matchers
48
+ @@matchers || {}
49
+ end
50
+
51
+ # Initialize is only called if a packet hasn't already been called for
52
+ # this env. Sets up the basic env, creating a pointer back to self
53
+ # and a Rack::Request object.
54
+ # @param [Orange::Core] orange a pointer to the orange core
55
+ # @param [Hash] env a standard Rack hash
56
+ def initialize(orange, env)
57
+ @orange = orange
58
+ @env = env
59
+ @env['orange.packet'] = self
60
+ @env['orange.env'] = {} unless @env['orange.env']
61
+ @env['orange.env'][:request] = Rack::Request.new(env)
62
+ @env['orange.env'][:headers] = {}
63
+ end
64
+
65
+ # Gives access to the orange.env key in the env, optionally
66
+ # including a default if key isn't involved.
67
+ # @param [Symbol, String] key the key to be found
68
+ # @param [optional, Object] default the return value if key doesn't exist (default is false)
69
+ # @return [Object] any value stored in the env
70
+ def [](key, default = false)
71
+ @env['orange.env'].has_key?(key) ? @env['orange.env'][key] : default
72
+ end
73
+
74
+ # Lets user set a value in the orange.env
75
+ # @param [Symbol, String] key the key to be set
76
+ # @param [Object] val the value to be set
77
+ def []=(key, val)
78
+ @env['orange.env'][key] = val
79
+ end
80
+
81
+ # Access to the main env (orange env is stored within the main env)
82
+ # @return [Hash] the request's env hash
83
+ def env
84
+ @env
85
+ end
86
+
87
+ # Access to the rack session
88
+ # @return [Hash] the session information made available by Rack
89
+ def session
90
+ env['rack.session']["flash"] ||= {}
91
+ env['rack.session']
92
+ end
93
+
94
+ # Access to the rack session flash
95
+ # @return [String] the string stored in the flash
96
+ def flash(key = nil, val = nil)
97
+ env['rack.session']["flash"] ||= {}
98
+ if key.nil? && val.nil?
99
+ env['rack.session']["flash"]
100
+ elsif val.nil?
101
+ env['rack.session']["flash"].delete(key)
102
+ else
103
+ env['rack.session']["flash"][key] = val
104
+ end
105
+ end
106
+
107
+ # Generate headers for finalization
108
+ # @return [Hash] the header information stored in the orange.env, combined with the defaults
109
+ # set as DEFAULT_HEADERS
110
+ def headers
111
+ packet[:headers, {}].with_defaults(DEFAULT_HEADERS)
112
+ end
113
+
114
+ # Set a header
115
+ # @param [String] key the key to be set
116
+ # @param [Object] val the value to be set
117
+ def header(key, val)
118
+ @env['orange.env'][:headers][key] = val
119
+ end
120
+
121
+ # Set a header (same as #header)
122
+ # @param [String] key the key to be set
123
+ # @param [Object] val the value to be set
124
+ def add_header(key, val)
125
+ header key, val
126
+ end
127
+
128
+ # Returns the content ready to be used by Rack (wrapped in an array)
129
+ # @return [Array] array of strings to be rendered
130
+ def content
131
+ # Stringify content if it isn't a string for some weird reason.
132
+ packet[:content] = packet[:content].to_s unless packet[:content].is_a? String
133
+ return [packet[:content]] if packet[:content]
134
+ return ['']
135
+ end
136
+
137
+ # Returns the request object generated by Rack::Request(packet.env)
138
+ # @return [Rack::Request] the request object
139
+ def request
140
+ packet[:request]
141
+ end
142
+
143
+ # A pointer to the Orange::Core instance
144
+ # @return [Orange::Core] the orange core run by the application
145
+ def orange
146
+ @orange
147
+ end
148
+
149
+ # Returns the array of [status, headers, content] Rack expects
150
+ # @return [Array] the triple array expected by Rack at the end
151
+ # of a call
152
+ def finish
153
+ headers = packet.headers
154
+ status = packet[:status, 200]
155
+ content = packet.content
156
+ if content.respond_to?(:to_ary)
157
+ headers["Content-Length"] = content.to_ary.
158
+ inject(0) { |len, part| len + Rack::Utils.bytesize(part) }.to_s
159
+ end
160
+ [status, headers, content]
161
+ end
162
+
163
+ # Returns self
164
+ # @return [Orange::Packet] self
165
+ def packet
166
+ self
167
+ end
168
+
169
+ # Includes the module passed
170
+ # @param [Module] inc the module to be mixed into the class
171
+ def self.mixin(inc)
172
+ include inc
173
+ end
174
+
175
+ # Route calls the router object set in the packet
176
+ # @return [void] route doesn't return anything directly, the
177
+ # main application calls packet.route then returns packet.finish.
178
+ # Routers set content, headers and status if necessary.
179
+ # They can also raise redirect errors to circumvent the process.
180
+ def route
181
+ router = packet['route.router']
182
+ raise 'Router not found' unless router
183
+ router.route(self)
184
+ end
185
+
186
+ # Pulls options set out of the packet and places them into
187
+ # a hash. Options are retrieved from POST, GET, and route.resource_path
188
+ # and take that order of precedence (POST overrides GET, etc)
189
+ # @param [Array] key_list an array of keys to retrieve and merge together
190
+ # in order of precedence. :GET and :POST for request vars, the rest
191
+ # are keys directly available on packet
192
+ # @return [Hash] A hash of options set in the packet
193
+ def extract_opts(key_list = [:POST, :GET, 'route.resource_path'])
194
+ opts = {}
195
+ key_list.reverse.each do |key|
196
+ case key
197
+ when :GET then opts.merge! packet.request.GET
198
+ when :POST then opts.merge! packet.request.POST
199
+ else
200
+ opts.merge!(packet[key, {}].kind_of?(String) ? url_to_hash(packet[key]) : packet[key, {}])
201
+ end
202
+ end
203
+ opts
204
+ end
205
+
206
+ # Converts a url path to a hash with symbol keys. URL must
207
+ # be in /key/val/key/val/etc format
208
+ def url_to_hash(url)
209
+ parts = url.split('/')
210
+ hash = {}
211
+ while !parts.blank?
212
+ key = parts.shift
213
+ key = parts.shift if key.blank?
214
+ val = parts.shift
215
+ hash[key.to_sym] = val unless key.blank? or val.blank?
216
+ end
217
+ hash
218
+ end
219
+
220
+ # Method Missing allows defining custom methods
221
+ def method_missing(id, *args)
222
+ matched = false
223
+ id = id.to_s
224
+ @@matchers.each_key do |k|
225
+ matched = k if id =~ k
226
+ break if matched
227
+ end
228
+ return @@matchers[matched].call(packet, matched.match(id), args) if matched
229
+ raise NoMethodError.new("No method ##{id} found", id)
230
+ end
231
+ end
232
+ end