scytrin-maveric 1.0.0 → 2

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