synfeld 0.0.4

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.
@@ -0,0 +1,281 @@
1
+ module Synfeld # :nodoc:
2
+
3
+ #
4
+ # See the synopsis section of README.rdoc for usage.
5
+ #
6
+ # See the README.rdoc for an overview of an Synfeld::App, and see the Rack::Mount project for
7
+ # more information on Rack::Mount style routing.
8
+ #
9
+ # Variables of note:
10
+ #
11
+ # @response
12
+ # a hash with keys :body, :headers, :status_code, the 3 items all rack handlers are expected to set.
13
+ # Body is a string, status code is an http status code integer, and headers is a hash that
14
+ # should conform to rack's contract.
15
+ #
16
+ # @env
17
+ # The rack env passed into this apps #call method
18
+ #
19
+ # @params
20
+ # The params as determined by the matching Rack::Mount route.
21
+ #
22
+ # @root_dir
23
+ # This dir is prepended to relative paths to locate files.
24
+ #
25
+ # @logger
26
+ # Either you pass in the @logger that synfeld uses, or it sets one up on STDOUT.
27
+ #
28
+ class App
29
+ attr_accessor :response, :params, :env, :root_dir, :logger
30
+
31
+ # Options:
32
+ # :logger => where to log to.
33
+ # Note this is not the same thing as the rack access log (although you
34
+ # can pass that logger in if you want). Default: Logger.new(STDOUT)
35
+ def initialize(opts = {})
36
+
37
+ @logger = opts[:logger]
38
+ if self.logger.nil?
39
+ @logger = Logger.new(STDOUT)
40
+ puts "WARNING: Synfeld not configured with a logger, using STDOUT. Won't have much to say if running as a daemon."
41
+ end
42
+
43
+ @root_dir = opts[:root_dir]
44
+ if self.root_dir.nil?
45
+ raise "You have to pass in the location of the 'root_dir', where all the files in your synfeld app are located"
46
+ end
47
+
48
+ Kernel.at_exit {self.whine("Alright, I'm outta here.")}
49
+ end
50
+
51
+ #
52
+ # RACK PLUMBING
53
+ #
54
+
55
+ # Return self as a rackup-able rack application.
56
+ def as_rack_app
57
+ #routes = Rack::Mount::RouteSet.new_without_optimizations do |set|
58
+ routes = Rack::Mount::RouteSet.new do |set|
59
+ @set = set
60
+ self.add_routes
61
+ add_route %r{^.*$}, :action => "render_static"
62
+ end
63
+ return routes
64
+ end
65
+
66
+ # The rack #call method
67
+ def call(env)
68
+ dup._call(env) # be thread-safe
69
+ end
70
+
71
+ #
72
+ # ROUTING
73
+ #
74
+
75
+ @@__regex_colon = (RUBY_VERSION =~ /^1.8/)? ':' : '' # :nodoc:
76
+
77
+ # See the README for a full explanation of how to use this method.
78
+ def add_route(string_or_regex, opts = {})
79
+ raise "You have to provide an :action method to call" unless opts[:action]
80
+ method = (opts.delete(:method) || 'GET').to_s.upcase
81
+ # Adapt string_or_regex into a rack-mount regex route. If it is a string, convert it to a
82
+ # rack-mount compatable regex. In paths that look like /some/:var/in/path, convert the ':var'
83
+ # bits to rack-mount variables.
84
+ if string_or_regex.is_a?(String)
85
+ regex_string = "^" + string_or_regex.gsub(/:(([^\/]+))/){|s| "(?#{@@__regex_colon}<#{$1}>.*)" } + "$"
86
+ regex = %r{#{regex_string}}
87
+ #puts regex_string # dbg
88
+ else
89
+ regex = string_or_regex
90
+ end
91
+
92
+ # Add the route to rack-mount
93
+ @set.add_route(self,
94
+ {:path_info => regex, :request_method => method.upcase},
95
+ opts)
96
+ end
97
+
98
+ #
99
+ # ACCESSORS & SUGAR
100
+ #
101
+
102
+ # The name of the action method bound to the route that mathed the incoming request.
103
+ def action
104
+ self.params[:action]
105
+ end
106
+
107
+ protected
108
+
109
+ def _call(env) # :nodoc:
110
+ begin
111
+ start_time = Time.now.to_f
112
+ @env = env
113
+ @params = env[ Rack::Mount::Const::RACK_ROUTING_ARGS ]
114
+ @response = {
115
+ :status_code => 200,
116
+ :headers => {'Content-Type' => 'text/html'},
117
+ :body => nil
118
+ }
119
+
120
+ action = self.action
121
+ if self.respond_to?(action)
122
+ result = self.send(self.action)
123
+ else
124
+ result = self.no_action
125
+ end
126
+
127
+ if result.is_a?(String)
128
+ response[:body] = result
129
+ else
130
+ raise "You have to set the response body" if response[:body].nil?
131
+ end
132
+
133
+ response[:headers]["Content-Length"] = response[:body].size.to_s
134
+
135
+ logger.debug("It took #{Time.now.to_f - start_time} sec for #{self.class} to handle request.")
136
+ [response[:status_code], response[:headers], Array(response[:body])]
137
+ rescue Exception => e
138
+ # It seems like we should get this next line for free from the CommonLogger, so I guess
139
+ # I'm doing something wrong, missing some piece of rack middleware or something. Until I
140
+ # figure it out, I'm explicitly logging the exception manually.
141
+ self.whine "#{e.class}, #{e}\n\t#{e.backtrace.join("\n\t")} "
142
+ raise e
143
+ end
144
+ end
145
+ # :startdoc:
146
+
147
+
148
+ #
149
+ # EXCEPTIONS
150
+ #
151
+
152
+ # send an error message to the log prepended by "Synfeld: "
153
+ def whine msg
154
+ logger.error("Synfeld laments: " + msg)
155
+ return msg
156
+ end
157
+
158
+
159
+ # Overrideable method that handles a missing action that was defined by a route
160
+ def no_action
161
+ self.response[:body] = "Action '#{self.action}' not found in '#{self.class}'"
162
+ self.response[:status_code] = 500
163
+ end
164
+
165
+ # Overrideable method that handles 404
166
+ def no_route
167
+ self.response[:body] = "route not found for: '#{self.env['REQUEST_URI']}'"
168
+ self.response[:status_code] = 404
169
+ end
170
+
171
+ #
172
+ # RENDERING
173
+ #
174
+
175
+ # Render an html file. 'fn' is a full path, or a path relative to @root_dir.
176
+ def render_html(fn)
177
+ F.read(full_path(fn))
178
+ end
179
+
180
+ # Serve up a blob of json (just sets Content-Type to 'text/javascript' and
181
+ # sets the body to the json passed in to this method).
182
+ def render_json(json)
183
+ self.response[:headers]['Content-Type'] = 'text/javascript'
184
+ self.response[:body] = json
185
+ end
186
+
187
+ # Render a haml file. 'fn' is a full path, or a path relative to @root_dir.
188
+ # 'locals' is a hash definining variables to be passed to the template.
189
+ def render_haml(fn, locals = {})
190
+
191
+ if not defined? Haml
192
+ begin
193
+ require 'haml'
194
+ rescue LoadError => x
195
+ return self.whine("Haml is not installed, required in order to render '#{fn}'")
196
+ end
197
+ end
198
+
199
+ Haml::Engine.new(F.read(full_path(fn)) ).render(Object.new, locals)
200
+ end
201
+
202
+ # Render an erb file. 'fn' is a full path, or a path relative to @root_dir.
203
+ # 'locals' is a hash definining variables to be passed to the template.
204
+ def render_erb(fn, locals = {})
205
+
206
+ if not defined? Erb
207
+ begin
208
+ require 'erb'
209
+ rescue LoadError => x
210
+ return self.whine("Erb is not installed, required in order to render '#{fn}'")
211
+ end
212
+ end
213
+
214
+ template = ERB.new F.read(full_path(fn))
215
+
216
+ bind = binding
217
+ locals.each do |n,v|
218
+ raise "Locals must be symbols. Not a symbol: #{n.inspect}" unless n.is_a?(Symbol)
219
+ eval("#{n} = locals[:#{n}]", bind)
220
+ end
221
+ template.result(bind)
222
+ end
223
+
224
+ def render_static
225
+ fn = F.expand_path(F.join(root_dir, self.env['REQUEST_URI']))
226
+ #puts fn # dbg
227
+ if F.exist?(fn) and not F.directory?(fn)
228
+ self.content_type!(fn.split('.').last)
229
+ F.read(fn)
230
+ else
231
+ return self.no_route
232
+ end
233
+ end
234
+
235
+ #
236
+ # UTIL
237
+ #
238
+
239
+ # Given a file extention, determine the 'Content-Type' and then set the
240
+ # @response[:headers]['Content-Type']. Unrecognized extentions are
241
+ # set to content type of 'text/plain'.
242
+ def content_type!(ext)
243
+ case ext.downcase
244
+ when 'haml'; t = 'text/html'
245
+ when 'erb'; t = 'text/html'
246
+ # I believe all the rest are determined accurately by the Rack::Mime.mime_type call in the else clause below.
247
+ # when 'html'; t = 'text/html'
248
+ # when 'js'; t = 'text/javascript'
249
+ # when 'css'; t = 'text/css'
250
+ # when 'png'; t = 'image/png'
251
+ # when 'gif'; t = 'image/gif'
252
+ # when 'jpg'; t = 'image/jpeg'
253
+ # when 'jpeg'; t = 'image/jpeg'
254
+ else t = Rack::Mime.mime_type('.' + ext, 'text/plain')
255
+ end
256
+ #puts("----#{ext}:" + t.inspect) # dbg
257
+ (self.response[:headers]['Content-Type'] = t) if t
258
+ end
259
+
260
+ # Return fn if a file by that name exists. If not, concatenate the @root_dir with the fn, and
261
+ # return that if it exists. Raise if the actual file cannot be determined.
262
+ #
263
+ # NOTE: no effort is made to protect access to files outside of your application's root
264
+ # dir. If you permit filepaths as request parameters, then it is up to you to make sure
265
+ # that they do not point to some sensitive part of your file-system.
266
+ def full_path(fn)
267
+ if F.exist?(fn)
268
+ return fn
269
+ elsif F.exist?(full_fn = F.join(self.root_dir, fn))
270
+ return full_fn
271
+ else
272
+ raise "Could not find file '#{fn}' (full path '#{full_fn}')"
273
+ end
274
+ end
275
+
276
+ end # class App
277
+
278
+ end # mod Synfeld
279
+
280
+
281
+
data/lib/synfeld.rb ADDED
@@ -0,0 +1,17 @@
1
+ F = ::File
2
+
3
+ # base ruby requires
4
+ require 'rubygems'
5
+ require 'logger'
6
+
7
+ # gems dependencies
8
+ require 'rubygems'
9
+
10
+ require 'rack'
11
+ require 'rack/mount'
12
+ require 'rack/mime'
13
+
14
+ # my files (require_all_libs_relative_to is a bones util method in synfeld_info.rb)
15
+ require F.join(File.dirname(__FILE__), 'synfeld_info')
16
+ Synfeld.require_all_libs_relative_to(__FILE__)
17
+
@@ -0,0 +1,51 @@
1
+ #
2
+ # See README.txt for usage.
3
+ #
4
+ module Synfeld
5
+
6
+ # :stopdoc:
7
+
8
+ VERSION = '0.0.4'
9
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
10
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
11
+
12
+ # Returns the version string for the library.
13
+ #
14
+ def self.version
15
+ VERSION
16
+ end
17
+
18
+ # Returns the library path for the module. If any arguments are given,
19
+ # they will be joined to the end of the libray path using
20
+ # <tt>File.join</tt>.
21
+ #
22
+ def self.libpath( *args )
23
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
24
+ end
25
+
26
+ # Returns the lpath for the module. If any arguments are given,
27
+ # they will be joined to the end of the path using
28
+ # <tt>File.join</tt>.
29
+ #
30
+ def self.path( *args )
31
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
32
+ end
33
+
34
+ # Utility method used to require all files ending in .rb that lie in the
35
+ # directory below this file that has the same name as the filename passed
36
+ # in. Optionally, a specific _directory_ name can be passed in such that
37
+ # the _filename_ does not have to be equivalent to the directory.
38
+ #
39
+ def self.require_all_libs_relative_to( fname, dir = nil )
40
+ dir ||= ::File.basename(fname, '.*')
41
+ search_me = ::File.expand_path(
42
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
43
+
44
+ Dir.glob(search_me).sort.each {|rb| require rb}
45
+ end
46
+
47
+ # :startdoc:
48
+
49
+ end # module Synfeld
50
+
51
+
@@ -0,0 +1,16 @@
1
+
2
+ require File.expand_path(
3
+ File.join(File.dirname(__FILE__), %w[.. lib synfeld]))
4
+
5
+ Spec::Runner.configure do |config|
6
+ # == Mock Framework
7
+ #
8
+ # RSpec uses it's own mocking framework by default. If you prefer to
9
+ # use mocha, flexmock or RR, uncomment the appropriate line:
10
+ #
11
+ # config.mock_with :mocha
12
+ # config.mock_with :flexmock
13
+ # config.mock_with :rr
14
+ end
15
+
16
+ # EOF
@@ -0,0 +1,7 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Synfeld do
5
+ end
6
+
7
+ # EOF
data/synfeld.gemspec ADDED
@@ -0,0 +1,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{synfeld}
5
+ s.version = "0.0.4"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Steven Swerling"]
9
+ s.date = %q{2009-09-25}
10
+ s.description = %q{Synfeld is a web application framework that does practically nothing.
11
+
12
+ Synfeld is little more than a small wrapper for Rack::Mount (see http://github.com/josh/rack-mount). If you want a web framework that is mostly just going to serve up json blobs, and occasionally serve up some simple content (eg. help files) and media, Synfeld makes that easy.
13
+
14
+ The sample app below shows pretty much everything there is to know about synfeld, in particular:
15
+
16
+ * How to define routes.
17
+ * Simple rendering of erb, haml, html, json, and static files.
18
+ * In the case of erb and haml, passing variables into the template is demonstrated.
19
+ * A dymamic action where the status code, headers, and body are created 'manually.'
20
+ * The erb demo link also demos the rendering of a partial (not visible in the code below, you have to look at the template file examples/public/erb_files/erb_test.erb).}
21
+ s.email = %q{sswerling@yahoo.com}
22
+ s.extra_rdoc_files = ["History.txt", "README.rdoc", "README.txt"]
23
+ s.files = [".gitignore", "History.txt", "README.rdoc", "README.txt", "Rakefile", "TODO", "TODO-rack-mount", "example/public/erb_files/erb_test.erb", "example/public/haml_files/haml_test.haml", "example/public/haml_files/home.haml", "example/public/html_files/html_test.html", "example/public/images/beef_interstellar_thm.jpg", "example/public/images/rails.png", "example/try_me.rb", "example/try_me.ru", "lib/synfeld.rb", "lib/synfeld/base.rb", "lib/synfeld_info.rb", "rackmount-test.ru", "spec/spec_helper.rb", "spec/synfeld_spec.rb", "synfeld.gemspec", "test/test_synfeld.rb"]
24
+ s.homepage = %q{http://tab-a.slot-z.net}
25
+ s.rdoc_options = ["--inline-source", "--main", "README.txt"]
26
+ s.require_paths = ["lib"]
27
+ s.rubyforge_project = %q{synfeld}
28
+ s.rubygems_version = %q{1.3.5}
29
+ s.summary = %q{Synfeld is a web application framework that does practically nothing}
30
+ s.test_files = ["test/test_synfeld.rb"]
31
+
32
+ if s.respond_to? :specification_version then
33
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
34
+ s.specification_version = 3
35
+
36
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
37
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
38
+ s.add_runtime_dependency(%q<rack-router>, [">= 0"])
39
+ s.add_development_dependency(%q<bones>, [">= 2.5.1"])
40
+ else
41
+ s.add_dependency(%q<rack>, [">= 0"])
42
+ s.add_dependency(%q<rack-router>, [">= 0"])
43
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
44
+ end
45
+ else
46
+ s.add_dependency(%q<rack>, [">= 0"])
47
+ s.add_dependency(%q<rack-router>, [">= 0"])
48
+ s.add_dependency(%q<bones>, [">= 2.5.1"])
49
+ end
50
+ end
data/tasks/ann.rake ADDED
@@ -0,0 +1,80 @@
1
+
2
+ begin
3
+ require 'bones/smtp_tls'
4
+ rescue LoadError
5
+ require 'net/smtp'
6
+ end
7
+ require 'time'
8
+
9
+ namespace :ann do
10
+
11
+ # A prerequisites task that all other tasks depend upon
12
+ task :prereqs
13
+
14
+ file PROJ.ann.file do
15
+ ann = PROJ.ann
16
+ puts "Generating #{ann.file}"
17
+ File.open(ann.file,'w') do |fd|
18
+ fd.puts("#{PROJ.name} version #{PROJ.version}")
19
+ fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
20
+ fd.puts(" #{PROJ.url}") if PROJ.url.valid?
21
+ fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
22
+ fd.puts
23
+ fd.puts("== DESCRIPTION")
24
+ fd.puts
25
+ fd.puts(PROJ.description)
26
+ fd.puts
27
+ fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
28
+ fd.puts
29
+ ann.paragraphs.each do |p|
30
+ fd.puts "== #{p.upcase}"
31
+ fd.puts
32
+ fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
33
+ fd.puts
34
+ end
35
+ fd.puts ann.text if ann.text
36
+ end
37
+ end
38
+
39
+ desc "Create an announcement file"
40
+ task :announcement => ['ann:prereqs', PROJ.ann.file]
41
+
42
+ desc "Send an email announcement"
43
+ task :email => ['ann:prereqs', PROJ.ann.file] do
44
+ ann = PROJ.ann
45
+ from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
46
+ to = Array(ann.email[:to])
47
+
48
+ ### build a mail header for RFC 822
49
+ rfc822msg = "From: #{from}\n"
50
+ rfc822msg << "To: #{to.join(',')}\n"
51
+ rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
52
+ rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
53
+ rfc822msg << "\n"
54
+ rfc822msg << "Date: #{Time.new.rfc822}\n"
55
+ rfc822msg << "Message-Id: "
56
+ rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
57
+ rfc822msg << File.read(ann.file)
58
+
59
+ params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
60
+ ann.email[key]
61
+ end
62
+
63
+ params[3] = PROJ.email if params[3].nil?
64
+
65
+ if params[4].nil?
66
+ STDOUT.write "Please enter your e-mail password (#{params[3]}): "
67
+ params[4] = STDIN.gets.chomp
68
+ end
69
+
70
+ ### send email
71
+ Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
72
+ end
73
+ end # namespace :ann
74
+
75
+ desc 'Alias to ann:announcement'
76
+ task :ann => 'ann:announcement'
77
+
78
+ CLOBBER << PROJ.ann.file
79
+
80
+ # EOF
data/tasks/bones.rake ADDED
@@ -0,0 +1,20 @@
1
+
2
+ if HAVE_BONES
3
+
4
+ namespace :bones do
5
+
6
+ desc 'Show the PROJ open struct'
7
+ task :debug do |t|
8
+ atr = if t.application.top_level_tasks.length == 2
9
+ t.application.top_level_tasks.pop
10
+ end
11
+
12
+ if atr then Bones::Debug.show_attr(PROJ, atr)
13
+ else Bones::Debug.show PROJ end
14
+ end
15
+
16
+ end # namespace :bones
17
+
18
+ end # HAVE_BONES
19
+
20
+ # EOF