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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.gems +4 -0
  3. data/.gitignore +2 -0
  4. data/CHANGELOG +47 -0
  5. data/CONTRIBUTING +19 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +23 -0
  8. data/README.md +67 -0
  9. data/app/.rspec +2 -0
  10. data/app/Gemfile +13 -0
  11. data/app/app/controllers/application_controller.rb +3 -0
  12. data/app/app/services/.keep +0 -0
  13. data/app/config.ru +4 -0
  14. data/app/config/environment.rb +9 -0
  15. data/app/config/initializers/.keep +0 -0
  16. data/app/config/routes.rb +7 -0
  17. data/app/config/settings.rb +0 -0
  18. data/app/spec/helpers.rb +2 -0
  19. data/app/spec/helpers/goofy.rb +11 -0
  20. data/app/spec/spec_helper.rb +107 -0
  21. data/benchmark/measure.rb +35 -0
  22. data/bin/check_its_goofy.rb +15 -0
  23. data/bin/goofy +61 -0
  24. data/bin/goofy_generator.rb +357 -0
  25. data/bin/goofy_instance_creator.rb +40 -0
  26. data/examples/config.ru +18 -0
  27. data/examples/measure.rb +17 -0
  28. data/examples/rack-response.ru +21 -0
  29. data/examples/views/home.mote +7 -0
  30. data/examples/views/layout.mote +11 -0
  31. data/goofy.gemspec +26 -0
  32. data/lib/goofy.rb +405 -0
  33. data/lib/goofy/capybara.rb +13 -0
  34. data/lib/goofy/controller.rb +14 -0
  35. data/lib/goofy/controller/base.rb +21 -0
  36. data/lib/goofy/controller/callbacks.rb +19 -0
  37. data/lib/goofy/render.rb +63 -0
  38. data/lib/goofy/router.rb +9 -0
  39. data/lib/goofy/safe.rb +23 -0
  40. data/lib/goofy/safe/csrf.rb +47 -0
  41. data/lib/goofy/safe/secure_headers.rb +40 -0
  42. data/lib/goofy/test.rb +11 -0
  43. data/makefile +4 -0
  44. data/test/accept.rb +32 -0
  45. data/test/captures.rb +162 -0
  46. data/test/composition.rb +69 -0
  47. data/test/controller.rb +29 -0
  48. data/test/cookie.rb +34 -0
  49. data/test/csrf.rb +139 -0
  50. data/test/extension.rb +21 -0
  51. data/test/helper.rb +11 -0
  52. data/test/host.rb +29 -0
  53. data/test/integration.rb +114 -0
  54. data/test/match.rb +86 -0
  55. data/test/middleware.rb +46 -0
  56. data/test/number.rb +36 -0
  57. data/test/on.rb +157 -0
  58. data/test/param.rb +66 -0
  59. data/test/path.rb +86 -0
  60. data/test/plugin.rb +68 -0
  61. data/test/rack.rb +22 -0
  62. data/test/redirect.rb +21 -0
  63. data/test/render.rb +128 -0
  64. data/test/root.rb +83 -0
  65. data/test/run.rb +23 -0
  66. data/test/safe.rb +74 -0
  67. data/test/segment.rb +45 -0
  68. data/test/session.rb +21 -0
  69. data/test/settings.rb +52 -0
  70. data/test/views/about.erb +1 -0
  71. data/test/views/about.str +1 -0
  72. data/test/views/content-yield.erb +1 -0
  73. data/test/views/custom/abs_path.mote +1 -0
  74. data/test/views/frag.mote +1 -0
  75. data/test/views/home.erb +2 -0
  76. data/test/views/home.mote +1 -0
  77. data/test/views/home.str +2 -0
  78. data/test/views/layout-alternative.erb +2 -0
  79. data/test/views/layout-yield.erb +3 -0
  80. data/test/views/layout.erb +2 -0
  81. data/test/views/layout.mote +2 -0
  82. data/test/views/layout.str +2 -0
  83. data/test/views/test.erb +1 -0
  84. data/test/with.rb +42 -0
  85. metadata +271 -0
@@ -0,0 +1,7 @@
1
+ This is the body
2
+
3
+ <ul>
4
+ % list.each do |e|
5
+ <li>{{ e }}</li>
6
+ % end
7
+ </ul>
@@ -0,0 +1,11 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="utf-8">
4
+
5
+ <title>Home</title>
6
+ </head>
7
+
8
+ <body>
9
+ {{ content }}
10
+ </body>
11
+ </html>
@@ -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
@@ -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] = {}