scytrin-maveric 1.0.0 → 2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +27 -15
  2. data/lib/maveric.rb +215 -204
  3. metadata +8 -7
data/README CHANGED
@@ -1,3 +1,5 @@
1
+ - Maveric
2
+
1
3
  Maveric is a MVC overlay for Rack.
2
4
 
3
5
  The dominant idea behind Maveric is as little magic as possible, being as
@@ -7,7 +9,7 @@ The use of a Maveric is simple.
7
9
 
8
10
  class MyApp < Maveric
9
11
  def get
10
- [200, {'Content-Type' => 'text/plain'}, 'Hello World!']
12
+ render {'<p>Hello World!</p>'}
11
13
  end
12
14
  end
13
15
 
@@ -21,24 +23,34 @@ By default the method used to generate a response is the lower case form of the
21
23
  http method. GET requests call #get, POST requests call #post, and so on. A 404
22
24
  is returned if a the appropriate method is not defined.
23
25
 
26
+ -- Actions
24
27
 
25
- To extend this behaviour you may define one or more routes. Routes are handled
26
- internally by the application. Routes are defined in the manner of
27
- uri-templates.
28
-
29
- The first route matching the PATH_INFO of the request is used, and if there is a
30
- value labeled "action" and a method by that name is defined, that method will be
31
- used in preference to the default. If no method by that name is found, or no
32
- matching route is found, default behaviour takes place.
28
+ To override this you may redefine #action= as needed.
33
29
 
34
30
  class MyApp < Maveric
35
- route '/{action}'
31
+ def action= method
32
+ if method == :get and @request.path_info == '/'
33
+ @action = :index
34
+ else
35
+ super
36
+ end
37
+ end
36
38
  def index
37
- [200, {'Content-Type' => 'text/plain'}, 'Hello World!']
39
+ render {'<p>Hello World!</p>'}
38
40
  end
39
41
  end
40
42
 
41
- env['PATH_INFO'] # => '/index'
42
- MyApp.call(env)[0] # => 200
43
- env['PATH_INFO'] # => '/'
44
- MyApp.call(env)[0] # => 404
43
+ -- The Render Method
44
+
45
+ Provides a simple way to format data.
46
+
47
+ class MyApp < Maveric
48
+ module Views
49
+ def html data
50
+ '<p>'+data.to_s+'</p>'
51
+ end
52
+ end
53
+ def get
54
+ render :html { 'Hello World!' }
55
+ end
56
+ end
data/lib/maveric.rb CHANGED
@@ -1,245 +1,256 @@
1
+ # Author: scytrin@stadik.net
2
+ require 'uri'
1
3
  require 'rack'
2
- require 'addressable/uri'
3
-
4
- class Maveric < Rack::Response
5
- # Simple logger. Defaults all output to STDERR.
6
- class << LOG = Object.new
7
- def method_missing(m, *a, &b)
8
- l = a.first.is_a?(String) ? ': '+a.shift : ''
9
- STDERR.puts m.to_s.upcase+':'+caller[0].gsub(/(.*):(.*):.*/,'\1:\2')+l
10
- STDERR.puts ' '+a.map{|e|e.inspect}*', '
11
- end
12
- end
13
- LOG.debug 'Building Maveric'
14
4
 
15
- # Response generator proc
16
- R = Hash.new{|h,k| Proc.new{|b| [k, {'Content-Type'=>'text/plain'}, [*b]] } }
17
- R[303] = proc{|l| [303, {'Location'=>l}, [] ] }
18
- R[:html] = proc{|b| [200, {'Content-Type'=>'text/html'}, [*b]] }
19
- R[:json] = proc{|b| [200, {'Content-Type'=>'application/json'}, [*b.to_json]] }
20
-
21
- # Builds a URLMap from the provided hash as well as Maverics
22
- #
23
- # Maverics are appended to the end of a Rack::Cascade if there is a collision
24
- # on the path.
25
- def self.Map map
26
- Controllers.each do |a|
27
- m = a.mount || a.name.downcase.gsub(/^|::/,'/')
28
- v = map[m]
29
- if v.is_a? Rack::Cascade
30
- v << a
31
- else
32
- v = !v ? a : Rack::Cascade.new([map[m],a]))
33
- map[m] = v
5
+ module Maveric
6
+ class Response < Exception
7
+ DEFAULT, @r = [500, 'text/plain'], {}
8
+ def self.[] e; @r[e]; end
9
+ def self.[]= e, v; @r[e]=v; end
10
+ def initialize err
11
+ @status, @headers, @body = if resp = Response[err] then resp else
12
+ [ DEFAULT[0],
13
+ { 'Content-Type' => DEFAULT[1],
14
+ 'Content-Length' => err.length.to_s },
15
+ err]
34
16
  end
17
+ super @body
35
18
  end
36
- Rack::URLMap.new(map)
19
+ def each; @body.each{|line| yield line }; end
20
+ def to_a; [@status, @headers, self]; end # to_splat in 1.9
21
+ alias_method :finish, :to_a
37
22
  end
38
23
 
39
- ### Begin controller stuffs ###
24
+ module Views
25
+ attr_reader :maveric
26
+ TEMPLATES = {}
27
+ end
40
28
 
41
- # Holds an array of all created controllers.
42
- Controllers = []
43
- # Helpers is included into Maverics on instatiation as well as the sandbox
44
- # used during rendering.
45
- module Helpers; end
46
- # A utility modules to hold references to model related actions and objects.
47
- module Models; end
48
- # Views is included into the sandbox utilized during rendering.
49
- module Views; end
29
+ module Helpers
30
+ protected
50
31
 
51
- # Default headers for Maveric instantiation.
52
- @header = {}
53
- # An array of preperations to perform on a Maveric at initialization.
54
- @preparations = []
32
+ # Powerhouse method to build a response. TODO: doc
33
+ def respond *args, &blk
34
+ headers = Hash === args.last ? args.pop : {}
35
+ seed = (Symbol === args.last && args.last) ? '' : args.pop
36
+ views, args = args.reverse.partition{|e| Symbol === e }
55
37
 
56
- class << self
57
- attr_reader :header, :preparations
58
- attr_accessor :mount
38
+ bad = views.map{|e|e.to_s} - self.class::Views.instance_methods
39
+ raise ArgumentError, 'Invalid views: '+bad.inspect unless bad.empty?
40
+ warn 'Invalid arguments: '+args.inspect unless args.empty?
59
41
 
60
- # When a Maveric is subclassed, default headers and preparations are
61
- # inherited. The Views, Modules, and Helpers modules are also included
62
- # into the subclass' corresponding modules. By default the mount point
63
- # of the subclass is the class' transformed full name.
64
- #
65
- # For example: Object would be '/object', Module would be '/module',
66
- # Maveric::LOG would be '/maveric/log', and Rack::Auth::OpenID would be
67
- # '/rack/auth/openid'.
68
- def inherited klass
69
- ::Maveric::Controllers << klass
70
- LOG.debug 'New MVC', klass
71
- s = self
72
- %w'Views Models Helpers'.each do |c|
73
- klass.const_set(c, Module.new).module_eval{ include s.const_get(c) }
74
- end
75
- %w'@header @preparations'.each do |v|
76
- klass.instance_variable_set v, s.instance_variable_get(v).dup
42
+ response = Rack::Response.new
43
+ response.headers.update self.class.headers
44
+ response.headers.update headers.
45
+ reject{|(k,v)| !k.is_a? String or !v.is_a? String }
46
+ response.extend self.class::Views
47
+ response.instance_variable_set '@maveric', self
48
+
49
+ catch :response do
50
+ yield response if block_given?
51
+ body = views.inject(seed){|b,v| response.__send__(v,b) }.to_s
52
+ response.length = Rack::Utils.bytesize body
53
+ response.body = body.to_a
54
+ return response.finish
77
55
  end
78
- klass.instance_variable_set :@mount, klass.to_s.downcase.gsub(/^|:+/, '/')
79
- super
80
56
  end
81
57
 
82
- # As a short cut to utilizing Maverics, a call to this method would string
83
- # the method calls of .new, #prepare, and #finish. The calls are
84
- # encapsulated in a catch block with the key of :response.
85
- def call env
86
- LOG.debug 'MVC:Args', *env.values_at('HTTP_HOST','REQUEST_URI','SCRIPT_NAME','PATH_INFO')
87
- # either throw(:response, [status,header,body]) or don't, pls
88
- response = catch(:response){ new(env).prepare.finish }
89
- LOG.debug 'MVC:Response', response
90
- response
91
- end
58
+ private
92
59
 
93
- # This utility method is used to set up preparations for Maverics on
94
- # instantiation. Preparations are inherited at the time of subclassing.
95
- #
96
- # If +name+ is the only argument, all preparations with the provided label
97
- # is returned.
98
- #
99
- # If opts or a block is provided, it is interpreted as a call to create a
100
- # new preparation. If a preparation with the given name already exists, an
101
- # exception will be raised.
102
- #
103
- # A preparation is a hash with the keys +:name+ and +:run+ set at a
104
- # minimum. +:name+ is set to the label provided as the first argument.
105
- # +:run+ is set to the block provided, or nil.
106
- #
107
- # When preparations are run, if +:run: is set, the block is evaluated in
108
- # the instances scope, otherwise the method named by +:name+ is called if
109
- # it exists.
110
- def preparation name, opts={}, &blk
111
- unless name.is_a? String
112
- raise ArgumentError, "name #{name.inspect} is not a string"
113
- end
114
- prep = @preparations.find{|e| e[:name] == name }
115
- if opts or blk
116
- if prep
117
- raise "Preparation with the name #{name.inspect} already exists"+
118
- " for #{self.inspect}."
119
- else
120
- @preparations << prep = opts.merge(:name => name, :run => blk)
121
- end
122
- end
123
- prep
60
+ # Provides a simple way of generating a redirection response, relative to
61
+ # the current url.
62
+ def redirect uri
63
+ uri = URI(uri).normalize
64
+ uri = URI(@request.url).merge uri
65
+ [ 303, { 'Location'=>uri.to_s,
66
+ 'Content-Type'=>'text/plain',
67
+ 'Content-Length'=>'0'
68
+ }, [] ]
124
69
  end
125
70
  end
126
71
 
127
- attr_reader :request, :action
128
72
 
129
- def initialize env
73
+
74
+ # The current instance is stored in the passed environment under the key
75
+ # 'maveric'. A Rack::Request of the current request is provided at @request.
76
+ #
77
+ # If no action is provided, @action is set as :index.
78
+ def initialize env, action=nil, route=nil
79
+ env['maveric'] = self
130
80
  @request = Rack::Request.new env
131
- @action = (@request.request_method||'get').downcase.to_sym
81
+ @route, @action = route, action || :index
132
82
  extend self.class::Helpers
133
- super [], 200, self.class.header
134
- LOG.debug 'MVC:Init', @action, self
83
+ self.init_hook if self.respond_to? :init_hook
84
+ p [ @request.script_name, @route, @action ]
135
85
  end
86
+ attr_reader :request, :route, :action
87
+
88
+
89
+ # Will return a 404 type Response if the set action is invalid. Will
90
+ # generate a new response if a previous one is not cached or +renew+ is true.
91
+ #
92
+ # If the response responds to :finish, it will return the result, otherwise
93
+ # the response itself will be returned.
94
+ def response renew=false
95
+ raise Response, 404 unless @action and respond_to? @action
96
+ @response = __send__ @action if not @response or renew
97
+ rescue Response
98
+ warn('Response: '+$!.inspect)
99
+ @response = $!.to_a
100
+ rescue
101
+ warn('Rescue: '+$!.inspect)
102
+ body = $!.message + "\n\t" + $@[0..10]*"\n\t"
103
+ @request.env['rack.errors'].puts body
104
+ body << "\n\t" << $@[11..-1]*"\n\t"
105
+ @request.env.sort.each{|(k,v)| body << "\n#{k}\n\t#{v.inspect}" }
106
+ @response = [ 500, {
107
+ 'Content-Type' => 'text/plain',
108
+ 'Content-Length' => body.length.to_s
109
+ }, body.to_a ]
110
+ end
111
+
112
+
113
+ # Methods provided to construct a maveric
114
+ module Class
115
+ # Default headers for Maveric instantiation.
116
+ attr_reader :routes, :headers
117
+ attr_writer :mount
118
+
119
+ def add_templates directory='templates/*'
120
+ Dir.glob directory do |file| p file
121
+ next unless File.readable? file and tmpl = File.read(file)
122
+ next unless extn = File.extname(file) and not extn.empty?
123
+ next unless name = File.basename(file, extn) and not name.empty?
124
+ view = case extn
125
+ when '.haml'
126
+ require 'haml' rescue next
127
+ puts "Defining HAML template view " + name.inspect
128
+ haml = Haml::Engine.new tmpl
129
+ proc{|content| haml.render self, :content => content }
130
+ when '.erb'
131
+ require 'erb' rescue next
132
+ puts "Defining ERB template view " + name.inspect
133
+ erb = ERB.new tmpl
134
+ erb.filename = file
135
+ proc{|content| erb.result(binding) }
136
+ else
137
+ puts "Defining sprintf template view " + name.inspect
138
+ proc{|content| sprintf self, content }
139
+ end
136
140
 
137
- def prepare
138
- self.class.preparations.each do |prep|
139
- next if prep.key? :except and !prep[:except].include?(@action)
140
- next if prep.key? :accept and prep[:accept].include?(@action)
141
- if prep[:run]
142
- instance_eval &prep[:run]
143
- elsif respond_to? prep[:name]
144
- __send__ prep[:name]
145
- else
146
- warn "Bad preparation for #{prep.inspect} in #{self}."
141
+ self::Views::TEMPLATES[name] = view
142
+ self::Views.__send__ :define_method, name, &view
147
143
  end
148
144
  end
149
- LOG.debug 'MVC:Prep', @action, self
150
- return self
151
- end
152
145
 
153
- def finish action=@action
154
- throw :response, Maveric::R[404]['Not Found, '+@request.fullpath] unless respond_to? @action
155
- __send__ action
156
- LOG.debug 'MVC:Fini', @action, self
157
- super()
158
- end
146
+ def call env
147
+ r, a = @routes.find{|(r,d)| r === env['PATH_INFO'] }
148
+ new(env, a, r).response
149
+ end
159
150
 
160
- protected
161
-
162
- # for starting content: use first arg unless a symbol, else i_eval given block
163
- # iterate through rest of args, calling matching method with arg of content
164
- # set @response.body to result... change?
165
- def render *args, &blk
166
- o = Object.new
167
- o.extend self.class::Views
168
- o.extend self.class::Helpers
169
- instance_variables.each do |i|
170
- o.instance_variable_set i, instance_variable_get(i)
151
+ def route d, *a
152
+ a.flatten.each{|r| @routes << [r, d] }
171
153
  end
172
- seed = args.first and !args.first.is_a?(Symbol) ? args.shift :
173
- block_given? ? o.instance_eval(&blk) : ''
174
- result = args.inject(seed) do |memo, view|
175
- next o.__send__(view, memo) if o.respond_to? view
176
- warn "No view <#{view}> in #{self.inspect}"
177
- memo
154
+
155
+ def mount
156
+ @mount || self.name.downcase.gsub(/^|::/, '/')
178
157
  end
179
- @body = result || seed
180
- end
181
158
 
182
-
183
- class URIProcessor
184
- DEF_MATCH = '.*'
185
- def initialize
186
- #uri.extract_mapping(String pattern, URIProcessor proc)
187
- @match = Hash.new(DEF_MATCH) #String of regex to match pattern
188
- @restore = {} #Transform to usable
189
- #URI.expand_template(String template, Hash mapping, URIProcessor proc)
190
- @validate = {} #Boolean if acceptable
191
- @transform = {} #Transform to uri encoding
192
- yield self if block_given?
159
+ def inspect
160
+ "#<Maveric:#{self.name} '#{mount}'>"
193
161
  end
194
- def match name, pattern=nil
195
- return @match[name] = pattern if pattern
196
- @match[name]
162
+
163
+ def self.extend_object obj
164
+ obj.instance_variable_set '@routes', []
165
+ obj.instance_variable_set '@headers', Rack::Utils::HeaderHash.new
166
+ super
197
167
  end
198
- def restore name, value=nil, &blk
199
- return @restore[name] = blk if blk
200
- return @restore[name].call(value) if value and @restore.key? name
201
- value
168
+ end
169
+
170
+ @maverics = [] unless defined? @maverics
171
+ @last_hash = nil
172
+ @map = nil
173
+ class << self
174
+ attr_reader :maverics
175
+
176
+ # Generates a Rack::URLMap compatible hash composed of all maverics and
177
+ # their mount points.
178
+ def map
179
+ return @map if @map and @last_hash == @maverics.hash
180
+ @last_hash = @maverics.hash
181
+ @map = @maverics.inject({}) do |h,m|
182
+ h.store m.mount, m
183
+ h
184
+ end
202
185
  end
203
- def validate name, value=nil, &blk
204
- return @validate[name] = blk if blk
205
- return @validate[name].call(value) if value and @validate.key? name
206
- !!@match[name].match(value)
186
+
187
+ # Generates a Rack::URLMap from the result of Maveric.map
188
+ def urlmap
189
+ return @urlmap if @urlmap and @last_hash == @maverics.hash
190
+ @urlmap = Rack::URLMap.new self.map
207
191
  end
208
- def transform name, value=nil, &blk
209
- return @transform[name] = blk if blk
210
- return @transform[name].call(value) if value and @transform.key? name
211
- value
192
+
193
+ # Maps to a call to the result of Maveric.urlmap
194
+ def call env
195
+ self.urlmap.call env
212
196
  end
213
- end
214
197
 
215
- Router = Hash.new{|h,k|h[k]=[]}
216
- def Router.route maveric, route=nil
217
- if route
218
- raise unless ::Maveric > maveric
219
- self[maveric] << r = [route, URIProcessor.new]
220
- return r
198
+ def new app
199
+ Rack::Cascade.new [app, self]
200
+ end
201
+
202
+ # When a maveric is subclassed, the superclass' headers inherited. Views
203
+ # and Helpers modules are also included into the subclass' corresponding
204
+ # modules.
205
+ #
206
+ # By default, the first class to inherit from Maveric is assigned the mount
207
+ # point of '/'.
208
+ #
209
+ # A maveric's default mount point is by default derived from it's full
210
+ # name. For example: Object would be '/object', Module would be '/module',
211
+ # and Rack::Auth::OpenID would be '/rack/auth/openid'.
212
+ def included obj
213
+ super
214
+ obj.extend Maveric::Class
215
+ %w'Helpers Views'.each do |name|
216
+ next if obj.const_defined?(name)
217
+ obj.const_set(name, Module.new).
218
+ instance_eval{ include Maveric.const_get(name) }
219
+ end
220
+ obj.mount = '/' if @maverics.empty?
221
+ @maverics << obj
221
222
  end
222
- raise unless maveric.is_a? ::Maveric
223
- self[maveric.class].find do |route, processor|
224
- uri = Addressable::URI.parse(maveric.request.path_info)
225
- map = uri.extract_mapping(route, processor)
226
- break map if map and map.all?{|k,v| !v.empty? }
227
- end || {}
228
- end
229
223
 
230
- # Add a route to the Maveric
231
- def self.route pattern
232
- Router.route self, pattern
224
+ def extend_object obj
225
+ warn 'Maveric should only be included.'
226
+ return obj
227
+ end
233
228
  end
229
+ end
234
230
 
235
- # Routing preparation for Maverics.
236
- # Calls Router#route with the maveric as the argument.
237
- # The resulting hash is assigned to @route.
238
- # If the value of 'action' exists within @route and the maveric has such
239
- # a named method, the value is assigned to @action.
240
- preparation :route do
241
- @route = Router.route self
242
- LOG.debug 'MVC:Route', @route
243
- a = @route['action'] and respond_to? a and @action = a.to_sym
231
+ class Rack::Request
232
+ # Allows the request to provide the current maveric.
233
+ def maveric; @env['maveric']; end
234
+ # Allows the request to provide the current session.
235
+ def session; @env['rack.session']; end
236
+ # Same as the standard Rack::Request method.
237
+ def url; site_root + fullpath; end
238
+ # Provides the compliment to #fullpath.
239
+ def site_root
240
+ url = scheme + "://" + host
241
+ url << ":#{port}" if \
242
+ scheme == "https" && port != 443 || \
243
+ scheme == "http" && port != 80
244
+ url
244
245
  end
245
246
  end
247
+
248
+ Maveric::Response[401] = [401,
249
+ {'Content-Type'=>'text/plain','Content-Length'=>'13'},
250
+ ['Unauthorized.']]
251
+ Maveric::Response[403] = [403,
252
+ {'Content-Type'=>'text/plain','Content-Length'=>'15'},
253
+ ['Not authorized.']]
254
+ Maveric::Response[404] = [404,
255
+ {'Content-Type'=>'text/plain','Content-Length'=>'10'},
256
+ ['Not Found.']]
metadata CHANGED
@@ -1,19 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scytrin-maveric
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: "2"
5
5
  platform: ruby
6
6
  authors:
7
- - blink
7
+ - scytrin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-05-09 00:00:00 -07:00
12
+ date: 2009-05-24 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
- name: addressable
16
+ name: rack
17
+ type: :runtime
17
18
  version_requirement:
18
19
  version_requirements: !ruby/object:Gem::Requirement
19
20
  requirements:
@@ -22,7 +23,7 @@ dependencies:
22
23
  version: "0"
23
24
  version:
24
25
  description:
25
- email: blinketje@gmail.com
26
+ email: scytrin@gmail.com
26
27
  executables: []
27
28
 
28
29
  extensions: []
@@ -32,12 +33,12 @@ extra_rdoc_files:
32
33
  files:
33
34
  - README
34
35
  - lib/maveric.rb
35
- has_rdoc: false
36
+ has_rdoc: true
36
37
  homepage: http://maveric.rubyforge.org/
37
38
  post_install_message:
38
39
  rdoc_options:
39
40
  - --main
40
- - README.txt
41
+ - README
41
42
  require_paths:
42
43
  - lib
43
44
  required_ruby_version: !ruby/object:Gem::Requirement