goofy 1.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.
- checksums.yaml +7 -0
- data/.gems +4 -0
- data/.gitignore +2 -0
- data/CHANGELOG +47 -0
- data/CONTRIBUTING +19 -0
- data/Gemfile +4 -0
- data/LICENSE +23 -0
- data/README.md +67 -0
- data/app/.rspec +2 -0
- data/app/Gemfile +13 -0
- data/app/app/controllers/application_controller.rb +3 -0
- data/app/app/services/.keep +0 -0
- data/app/config.ru +4 -0
- data/app/config/environment.rb +9 -0
- data/app/config/initializers/.keep +0 -0
- data/app/config/routes.rb +7 -0
- data/app/config/settings.rb +0 -0
- data/app/spec/helpers.rb +2 -0
- data/app/spec/helpers/goofy.rb +11 -0
- data/app/spec/spec_helper.rb +107 -0
- data/benchmark/measure.rb +35 -0
- data/bin/check_its_goofy.rb +15 -0
- data/bin/goofy +61 -0
- data/bin/goofy_generator.rb +357 -0
- data/bin/goofy_instance_creator.rb +40 -0
- data/examples/config.ru +18 -0
- data/examples/measure.rb +17 -0
- data/examples/rack-response.ru +21 -0
- data/examples/views/home.mote +7 -0
- data/examples/views/layout.mote +11 -0
- data/goofy.gemspec +26 -0
- data/lib/goofy.rb +405 -0
- data/lib/goofy/capybara.rb +13 -0
- data/lib/goofy/controller.rb +14 -0
- data/lib/goofy/controller/base.rb +21 -0
- data/lib/goofy/controller/callbacks.rb +19 -0
- data/lib/goofy/render.rb +63 -0
- data/lib/goofy/router.rb +9 -0
- data/lib/goofy/safe.rb +23 -0
- data/lib/goofy/safe/csrf.rb +47 -0
- data/lib/goofy/safe/secure_headers.rb +40 -0
- data/lib/goofy/test.rb +11 -0
- data/makefile +4 -0
- data/test/accept.rb +32 -0
- data/test/captures.rb +162 -0
- data/test/composition.rb +69 -0
- data/test/controller.rb +29 -0
- data/test/cookie.rb +34 -0
- data/test/csrf.rb +139 -0
- data/test/extension.rb +21 -0
- data/test/helper.rb +11 -0
- data/test/host.rb +29 -0
- data/test/integration.rb +114 -0
- data/test/match.rb +86 -0
- data/test/middleware.rb +46 -0
- data/test/number.rb +36 -0
- data/test/on.rb +157 -0
- data/test/param.rb +66 -0
- data/test/path.rb +86 -0
- data/test/plugin.rb +68 -0
- data/test/rack.rb +22 -0
- data/test/redirect.rb +21 -0
- data/test/render.rb +128 -0
- data/test/root.rb +83 -0
- data/test/run.rb +23 -0
- data/test/safe.rb +74 -0
- data/test/segment.rb +45 -0
- data/test/session.rb +21 -0
- data/test/settings.rb +52 -0
- data/test/views/about.erb +1 -0
- data/test/views/about.str +1 -0
- data/test/views/content-yield.erb +1 -0
- data/test/views/custom/abs_path.mote +1 -0
- data/test/views/frag.mote +1 -0
- data/test/views/home.erb +2 -0
- data/test/views/home.mote +1 -0
- data/test/views/home.str +2 -0
- data/test/views/layout-alternative.erb +2 -0
- data/test/views/layout-yield.erb +3 -0
- data/test/views/layout.erb +2 -0
- data/test/views/layout.mote +2 -0
- data/test/views/layout.str +2 -0
- data/test/views/test.erb +1 -0
- data/test/with.rb +42 -0
- metadata +271 -0
data/goofy.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "goofy"
|
3
|
+
s.version = "1.0.2"
|
4
|
+
s.summary = "Microframework for web applications"
|
5
|
+
s.description = "Goofy is a microframework for web applications(heavily based on Cuba)."
|
6
|
+
s.authors = ["Ehsan Yousefi"]
|
7
|
+
s.email = ["ehsan.yousefi@live.com"]
|
8
|
+
s.homepage = "https://github.com/EhsanYousefi/goofy"
|
9
|
+
s.license = "MIT"
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
14
|
+
|
15
|
+
s.add_dependency "rack", "~> 1.6.0"
|
16
|
+
s.add_dependency "prong", '~> 1.0'
|
17
|
+
s.add_dependency "require_all"
|
18
|
+
s.add_dependency "psych"
|
19
|
+
s.add_dependency "wisper"
|
20
|
+
s.add_dependency "racksh"
|
21
|
+
s.add_dependency "shotgun"
|
22
|
+
s.add_development_dependency "cutest"
|
23
|
+
s.add_development_dependency "rack-test"
|
24
|
+
s.add_development_dependency "tilt"
|
25
|
+
|
26
|
+
end
|
data/lib/goofy.rb
ADDED
@@ -0,0 +1,405 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
class Goofy
|
5
|
+
|
6
|
+
SLASH = "/".freeze
|
7
|
+
EMPTY = "".freeze
|
8
|
+
SEGMENT = "([^\\/]+)".freeze
|
9
|
+
DEFAULT = "text/html; charset=utf-8".freeze
|
10
|
+
|
11
|
+
class Response
|
12
|
+
LOCATION = "Location".freeze
|
13
|
+
|
14
|
+
attr_accessor :status
|
15
|
+
|
16
|
+
attr :body
|
17
|
+
attr :headers
|
18
|
+
|
19
|
+
def initialize(headers = {})
|
20
|
+
@status = nil
|
21
|
+
@headers = headers
|
22
|
+
@body = []
|
23
|
+
@length = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](key)
|
27
|
+
@headers[key]
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key, value)
|
31
|
+
@headers[key] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def write(str)
|
35
|
+
s = str.to_s
|
36
|
+
|
37
|
+
@length += s.bytesize
|
38
|
+
@headers[Rack::CONTENT_LENGTH] = @length.to_s
|
39
|
+
@body << s
|
40
|
+
end
|
41
|
+
|
42
|
+
def redirect(path, status = 302)
|
43
|
+
@headers[LOCATION] = path
|
44
|
+
@status = status
|
45
|
+
end
|
46
|
+
|
47
|
+
def finish
|
48
|
+
[@status, @headers, @body]
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_cookie(key, value)
|
52
|
+
Rack::Utils.set_cookie_header!(@headers, key, value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_cookie(key, value = {})
|
56
|
+
Rack::Utils.delete_cookie_header!(@headers, key, value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.reset!
|
61
|
+
@app = nil
|
62
|
+
@prototype = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.app
|
66
|
+
@app ||= Rack::Builder.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.use(middleware, *args, &block)
|
70
|
+
app.use(middleware, *args, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.define(&block)
|
74
|
+
app.run new(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.prototype
|
78
|
+
@prototype ||= app.to_app
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.call(env)
|
82
|
+
prototype.call(env)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.plugin(mixin)
|
86
|
+
include mixin
|
87
|
+
extend mixin::ClassMethods if defined?(mixin::ClassMethods)
|
88
|
+
|
89
|
+
mixin.setup(self) if mixin.respond_to?(:setup)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.settings
|
93
|
+
@settings ||= {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.deepclone(obj)
|
97
|
+
Marshal.load(Marshal.dump(obj))
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.inherited(child)
|
101
|
+
child.settings.replace(deepclone(settings))
|
102
|
+
end
|
103
|
+
|
104
|
+
attr :env
|
105
|
+
attr :req
|
106
|
+
attr :res
|
107
|
+
attr :captures
|
108
|
+
|
109
|
+
def initialize(&blk)
|
110
|
+
@blk = blk
|
111
|
+
@captures = []
|
112
|
+
end
|
113
|
+
|
114
|
+
def settings
|
115
|
+
self.class.settings
|
116
|
+
end
|
117
|
+
|
118
|
+
def call(env)
|
119
|
+
dup.call!(env)
|
120
|
+
end
|
121
|
+
|
122
|
+
def call!(env)
|
123
|
+
@env = env
|
124
|
+
@req = settings[:req].new(env)
|
125
|
+
@res = settings[:res].new(settings[:default_headers].dup)
|
126
|
+
|
127
|
+
# This `catch` statement will either receive a
|
128
|
+
# rack response tuple via a `halt`, or will
|
129
|
+
# fall back to issuing a 404.
|
130
|
+
#
|
131
|
+
# When it `catch`es a throw, the return value
|
132
|
+
# of this whole `call!` method will be the
|
133
|
+
# rack response tuple, which is exactly what we want.
|
134
|
+
catch(:halt) do
|
135
|
+
instance_eval(&@blk)
|
136
|
+
|
137
|
+
not_found
|
138
|
+
res.finish
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def session
|
143
|
+
env["rack.session"] || raise(RuntimeError,
|
144
|
+
"You're missing a session handler. You can get started " +
|
145
|
+
"by adding Goofy.use Rack::Session::Cookie")
|
146
|
+
end
|
147
|
+
|
148
|
+
# The heart of the path / verb / any condition matching.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
#
|
152
|
+
# on get do
|
153
|
+
# res.write "GET"
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# on get, "signup" do
|
157
|
+
# res.write "Signup"
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# on "user/:id" do |uid|
|
161
|
+
# res.write "User: #{uid}"
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# on "styles", extension("css") do |file|
|
165
|
+
# res.write render("styles/#{file}.sass")
|
166
|
+
# end
|
167
|
+
#
|
168
|
+
def on(*args, &block)
|
169
|
+
try do
|
170
|
+
# For every block, we make sure to reset captures so that
|
171
|
+
# nesting matchers won't mess with each other's captures.
|
172
|
+
@captures = []
|
173
|
+
|
174
|
+
# We stop evaluation of this entire matcher unless
|
175
|
+
# each and every `arg` defined for this matcher evaluates
|
176
|
+
# to a non-false value.
|
177
|
+
#
|
178
|
+
# Short circuit examples:
|
179
|
+
# on true, false do
|
180
|
+
#
|
181
|
+
# # PATH_INFO=/user
|
182
|
+
# on true, "signup"
|
183
|
+
return unless args.all? { |arg| match(arg) }
|
184
|
+
|
185
|
+
# The captures we yield here were generated and assembled
|
186
|
+
# by evaluating each of the `arg`s above. Most of these
|
187
|
+
# are carried out by #consume.
|
188
|
+
yield(*captures)
|
189
|
+
|
190
|
+
if res.status.nil?
|
191
|
+
if res.body.empty?
|
192
|
+
not_found
|
193
|
+
else
|
194
|
+
res.headers[Rack::CONTENT_TYPE] ||= DEFAULT
|
195
|
+
res.status = 200
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
halt(res.finish)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# @private Used internally by #on to ensure that SCRIPT_NAME and
|
204
|
+
# PATH_INFO are reset to their proper values.
|
205
|
+
def try
|
206
|
+
script, path = env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO]
|
207
|
+
|
208
|
+
yield
|
209
|
+
|
210
|
+
ensure
|
211
|
+
env[Rack::SCRIPT_NAME], env[Rack::PATH_INFO] = script, path
|
212
|
+
end
|
213
|
+
private :try
|
214
|
+
|
215
|
+
def consume(pattern)
|
216
|
+
matchdata = env[Rack::PATH_INFO].match(/\A\/(#{pattern})(\/|\z)/)
|
217
|
+
|
218
|
+
return false unless matchdata
|
219
|
+
|
220
|
+
path, *vars = matchdata.captures
|
221
|
+
|
222
|
+
env[Rack::SCRIPT_NAME] += "/#{path}"
|
223
|
+
env[Rack::PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
|
224
|
+
|
225
|
+
captures.push(*vars)
|
226
|
+
end
|
227
|
+
private :consume
|
228
|
+
|
229
|
+
def match(matcher, segment = SEGMENT)
|
230
|
+
case matcher
|
231
|
+
when String then consume(matcher.gsub(/:\w+/, segment))
|
232
|
+
when Regexp then consume(matcher)
|
233
|
+
when Symbol then consume(segment)
|
234
|
+
when Proc then matcher.call
|
235
|
+
else
|
236
|
+
matcher
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# A matcher for files with a certain extension.
|
241
|
+
#
|
242
|
+
# @example
|
243
|
+
# # PATH_INFO=/style/app.css
|
244
|
+
# on "style", extension("css") do |file|
|
245
|
+
# res.write file # writes app
|
246
|
+
# end
|
247
|
+
def extension(ext = "\\w+")
|
248
|
+
lambda { consume("([^\\/]+?)\.#{ext}\\z") }
|
249
|
+
end
|
250
|
+
|
251
|
+
# Ensures that certain request parameters are present. Acts like a
|
252
|
+
# precondition / assertion for your route. A default value can be
|
253
|
+
# provided as a second argument. In that case, it always matches
|
254
|
+
# and the result is either the parameter or the default value.
|
255
|
+
#
|
256
|
+
# @example
|
257
|
+
# # POST with data like user[fname]=John&user[lname]=Doe
|
258
|
+
# on "signup", param("user") do |atts|
|
259
|
+
# User.create(atts)
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
# on "login", param("username", "guest") do |username|
|
263
|
+
# # If not provided, username == "guest"
|
264
|
+
# end
|
265
|
+
def param(key, default = nil)
|
266
|
+
value = req[key] || default
|
267
|
+
|
268
|
+
lambda { captures << value unless value.to_s.empty? }
|
269
|
+
end
|
270
|
+
|
271
|
+
# Useful for matching against the request host (i.e. HTTP_HOST).
|
272
|
+
#
|
273
|
+
# @example
|
274
|
+
# on host("account1.example.com"), "api" do
|
275
|
+
# res.write "You have reached the API of account1."
|
276
|
+
# end
|
277
|
+
def host(hostname)
|
278
|
+
hostname === req.host
|
279
|
+
end
|
280
|
+
|
281
|
+
# If you want to match against the HTTP_ACCEPT value.
|
282
|
+
#
|
283
|
+
# @example
|
284
|
+
# # HTTP_ACCEPT=application/xml
|
285
|
+
# on accept("application/xml") do
|
286
|
+
# # automatically set to application/xml.
|
287
|
+
# res.write res["Content-Type"]
|
288
|
+
# end
|
289
|
+
def accept(mimetype)
|
290
|
+
lambda do
|
291
|
+
accept = String(env["HTTP_ACCEPT"]).split(",")
|
292
|
+
|
293
|
+
if accept.any? { |s| s.strip == mimetype }
|
294
|
+
res[Rack::CONTENT_TYPE] = mimetype
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# Syntactic sugar for providing catch-all matches.
|
300
|
+
#
|
301
|
+
# @example
|
302
|
+
# on default do
|
303
|
+
# res.write "404"
|
304
|
+
# end
|
305
|
+
def default
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
# Access the root of the application.
|
310
|
+
#
|
311
|
+
# @example
|
312
|
+
#
|
313
|
+
# # GET /
|
314
|
+
# on root do
|
315
|
+
# res.write "Home"
|
316
|
+
# end
|
317
|
+
def root
|
318
|
+
env[Rack::PATH_INFO] == SLASH || env[Rack::PATH_INFO] == EMPTY
|
319
|
+
end
|
320
|
+
|
321
|
+
# Syntatic sugar for providing HTTP Verb matching.
|
322
|
+
#
|
323
|
+
# @example
|
324
|
+
# on get, "signup" do
|
325
|
+
# end
|
326
|
+
#
|
327
|
+
# on post, "signup" do
|
328
|
+
# end
|
329
|
+
def get; req.get? end
|
330
|
+
def post; req.post? end
|
331
|
+
def put; req.put? end
|
332
|
+
def delete; req.delete? end
|
333
|
+
|
334
|
+
# If you want to halt the processing of an existing handler
|
335
|
+
# and continue it via a different handler.
|
336
|
+
#
|
337
|
+
# @example
|
338
|
+
# def redirect(*args)
|
339
|
+
# run Goofy.new { on(default) { res.redirect(*args) }}
|
340
|
+
# end
|
341
|
+
#
|
342
|
+
# on "account" do
|
343
|
+
# redirect "/login" unless session["uid"]
|
344
|
+
#
|
345
|
+
# res.write "Super secure account info."
|
346
|
+
# end
|
347
|
+
def run(app)
|
348
|
+
halt app.call(req.env)
|
349
|
+
end
|
350
|
+
|
351
|
+
def halt(response)
|
352
|
+
throw :halt, response
|
353
|
+
end
|
354
|
+
|
355
|
+
# Adds ability to pass information to a nested Goofy application.
|
356
|
+
# It receives two parameters: a hash that represents the passed
|
357
|
+
# information and a block. The #vars method is used to retrieve
|
358
|
+
# a hash with the passed information.
|
359
|
+
#
|
360
|
+
# class Platforms < Goofy
|
361
|
+
# define do
|
362
|
+
# platform = vars[:platform]
|
363
|
+
#
|
364
|
+
# on default do
|
365
|
+
# res.write(platform) # => "heroku" or "salesforce"
|
366
|
+
# end
|
367
|
+
# end
|
368
|
+
# end
|
369
|
+
#
|
370
|
+
# Goofy.define do
|
371
|
+
# on "(heroku|salesforce)" do |platform|
|
372
|
+
# with(platform: platform) do
|
373
|
+
# run(Platforms)
|
374
|
+
# end
|
375
|
+
# end
|
376
|
+
# end
|
377
|
+
#
|
378
|
+
def with(dict = {})
|
379
|
+
old, env["Goofy.vars"] = vars, vars.merge(dict)
|
380
|
+
yield
|
381
|
+
ensure
|
382
|
+
env["Goofy.vars"] = old
|
383
|
+
end
|
384
|
+
|
385
|
+
# Returns a hash with the information set by the #with method.
|
386
|
+
#
|
387
|
+
# with(role: "admin", site: "main") do
|
388
|
+
# on default do
|
389
|
+
# res.write(vars.inspect)
|
390
|
+
# end
|
391
|
+
# end
|
392
|
+
# # => '{:role=>"admin", :site=>"main"}'
|
393
|
+
#
|
394
|
+
def vars
|
395
|
+
env["Goofy.vars"] ||= {}
|
396
|
+
end
|
397
|
+
|
398
|
+
def not_found
|
399
|
+
res.status = 404
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
Goofy.settings[:req] = Rack::Request
|
404
|
+
Goofy.settings[:res] = Goofy::Response
|
405
|
+
Goofy.settings[:default_headers] = {}
|