bradphelan-sinatras-hat 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/.gitignore +4 -0
  2. data/LICENSE +22 -0
  3. data/README.md +235 -0
  4. data/Rakefile +59 -0
  5. data/VERSION +1 -0
  6. data/bradphelan-sinatras-hat.gemspec +145 -0
  7. data/ci.rb +9 -0
  8. data/example/app-with-auth.rb +14 -0
  9. data/example/app-with-cache.rb +30 -0
  10. data/example/app.rb +23 -0
  11. data/example/lib/comment.rb +13 -0
  12. data/example/lib/common.rb +19 -0
  13. data/example/lib/post.rb +16 -0
  14. data/example/simple-app.rb +4 -0
  15. data/example/views/comments/index.erb +6 -0
  16. data/example/views/comments/show.erb +1 -0
  17. data/example/views/posts/index.erb +8 -0
  18. data/example/views/posts/new.erb +11 -0
  19. data/example/views/posts/show.erb +28 -0
  20. data/features/authenticated.feature +12 -0
  21. data/features/create.feature +16 -0
  22. data/features/destroy.feature +18 -0
  23. data/features/edit.feature +17 -0
  24. data/features/formats.feature +19 -0
  25. data/features/headers.feature +28 -0
  26. data/features/index.feature +23 -0
  27. data/features/layouts.feature +11 -0
  28. data/features/nested.feature +20 -0
  29. data/features/new.feature +20 -0
  30. data/features/only.feature +13 -0
  31. data/features/show.feature +31 -0
  32. data/features/steps/authenticated_steps.rb +10 -0
  33. data/features/steps/common_steps.rb +77 -0
  34. data/features/steps/create_steps.rb +21 -0
  35. data/features/steps/destroy_steps.rb +16 -0
  36. data/features/steps/edit_steps.rb +7 -0
  37. data/features/steps/format_steps.rb +11 -0
  38. data/features/steps/header_steps.rb +7 -0
  39. data/features/steps/index_steps.rb +26 -0
  40. data/features/steps/nested_steps.rb +11 -0
  41. data/features/steps/new_steps.rb +15 -0
  42. data/features/steps/only_steps.rb +10 -0
  43. data/features/steps/show_steps.rb +24 -0
  44. data/features/steps/update_steps.rb +22 -0
  45. data/features/support/env.rb +17 -0
  46. data/features/support/views/comments/index.erb +5 -0
  47. data/features/support/views/layout.erb +9 -0
  48. data/features/support/views/people/edit.erb +1 -0
  49. data/features/support/views/people/index.erb +1 -0
  50. data/features/support/views/people/layout.erb +9 -0
  51. data/features/support/views/people/new.erb +1 -0
  52. data/features/support/views/people/show.erb +1 -0
  53. data/features/update.feature +25 -0
  54. data/lib/core_ext/array.rb +5 -0
  55. data/lib/core_ext/hash.rb +23 -0
  56. data/lib/core_ext/module.rb +14 -0
  57. data/lib/core_ext/object.rb +45 -0
  58. data/lib/sinatras-hat.rb +22 -0
  59. data/lib/sinatras-hat/actions.rb +81 -0
  60. data/lib/sinatras-hat/authentication.rb +55 -0
  61. data/lib/sinatras-hat/extendor.rb +24 -0
  62. data/lib/sinatras-hat/hash_mutator.rb +18 -0
  63. data/lib/sinatras-hat/logger.rb +36 -0
  64. data/lib/sinatras-hat/maker.rb +187 -0
  65. data/lib/sinatras-hat/model.rb +110 -0
  66. data/lib/sinatras-hat/resource.rb +57 -0
  67. data/lib/sinatras-hat/responder.rb +106 -0
  68. data/lib/sinatras-hat/response.rb +60 -0
  69. data/lib/sinatras-hat/router.rb +46 -0
  70. data/sinatras-hat.gemspec +34 -0
  71. data/spec/actions/create_spec.rb +68 -0
  72. data/spec/actions/destroy_spec.rb +58 -0
  73. data/spec/actions/edit_spec.rb +52 -0
  74. data/spec/actions/index_spec.rb +72 -0
  75. data/spec/actions/new_spec.rb +39 -0
  76. data/spec/actions/show_spec.rb +85 -0
  77. data/spec/actions/update_spec.rb +83 -0
  78. data/spec/extendor_spec.rb +78 -0
  79. data/spec/fixtures/views/articles/edit.erb +1 -0
  80. data/spec/fixtures/views/articles/index.erb +1 -0
  81. data/spec/fixtures/views/articles/new.erb +1 -0
  82. data/spec/fixtures/views/articles/show.erb +1 -0
  83. data/spec/hash_mutator_spec.rb +23 -0
  84. data/spec/maker_spec.rb +411 -0
  85. data/spec/model_spec.rb +152 -0
  86. data/spec/resource_spec.rb +74 -0
  87. data/spec/responder_spec.rb +139 -0
  88. data/spec/response_spec.rb +120 -0
  89. data/spec/router_spec.rb +105 -0
  90. data/spec/spec_helper.rb +80 -0
  91. metadata +161 -0
@@ -0,0 +1,57 @@
1
+ module Sinatra
2
+ module Hat
3
+ # Handles the logic of generating a path for a given resource,
4
+ # taking any and all parents into consideration.
5
+ class Resource
6
+ def initialize(maker)
7
+ @maker = maker
8
+ end
9
+
10
+ def path(suffix, record=nil)
11
+ records = record ? path_records_for(record) : []
12
+ results = resources.inject("") do |memo, maker|
13
+ memo += fragment(maker, record)
14
+ end
15
+
16
+ interpolate(clean(results + suffix.dup), records)
17
+ end
18
+
19
+ private
20
+
21
+ def interpolate(uri, records)
22
+ return uri if records.empty?
23
+ uri.gsub(/:(\w+)/) { records.pop.to_param }
24
+ end
25
+
26
+ def path_records_for(record)
27
+ [record].tap do |parents|
28
+ resources.reverse.each do |resource|
29
+ parents << resource.model.find_owner(parents.last.attributes)
30
+ parents.compact!
31
+ parents.uniq!
32
+ end
33
+ end
34
+ end
35
+
36
+ def fragment(maker, record)
37
+ @maker.eql?(maker) ?
38
+ "/#{maker.prefix}" :
39
+ "/#{maker.prefix}/" + key(maker)
40
+ end
41
+
42
+ def key(maker)
43
+ maker.model.foreign_key.inspect
44
+ end
45
+
46
+ def clean(s)
47
+ s.downcase!
48
+ s.gsub!(%r(/$), '')
49
+ s
50
+ end
51
+
52
+ def resources
53
+ @maker.parents + [@maker]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,106 @@
1
+ module Sinatra
2
+ module Hat
3
+ # The responder assigns data to instance variables, then either
4
+ # gets the appropriate response proc and instance_exec's it in the
5
+ # context of a new Response object, or serializes the data.
6
+ class Responder
7
+ delegate :model, :to => :maker
8
+
9
+ attr_reader :maker
10
+
11
+ def initialize(maker)
12
+ @maker = maker
13
+ end
14
+
15
+ def defaults
16
+ @defaults ||= {
17
+ :show => {
18
+ :success => proc { |data| render(:show) },
19
+ :failure => proc { |data| redirect('/') }
20
+ },
21
+
22
+ :index => {
23
+ :success => proc { |data| render(:index) },
24
+ :failure => proc { |data| redirect('/') }
25
+ },
26
+
27
+ :create => {
28
+ :success => proc { |data| redirect(data) },
29
+ :failure => proc { |data| render(:new) }
30
+ },
31
+
32
+ :new => {
33
+ :success => proc { |data| render(:new) },
34
+ :failure => proc { |data| redirect('/') }
35
+ },
36
+
37
+ :edit => {
38
+ :success => proc { |data| render(:edit) }
39
+ },
40
+
41
+ :destroy => {
42
+ :success => proc { |data| redirect(resource_path('/')) }
43
+ },
44
+
45
+ :update => {
46
+ :success => proc { |data| redirect(data) },
47
+ :failure => proc { |data| render(:edit) }
48
+ }
49
+ }
50
+ end
51
+
52
+ # Called when a request is handled successfully. For most GET
53
+ # requests, this is always the case. For update/create actions,
54
+ # it is when the record is created/updated successfully.
55
+ def success(name, request, data)
56
+ handle(:success, name, request, data)
57
+ end
58
+
59
+ # Called when a request is not able to handled. This could be
60
+ # because a record could not be created or saved.
61
+ def failure(name, request, data)
62
+ handle(:failure, name, request, data)
63
+ end
64
+
65
+ # Serializes the data passed in, first looking for a custom formatter,
66
+ # then falling back on trying to call to_[format] on the data. If neither
67
+ # are available, returns an error with the status code 406.
68
+ def serialize(data, format)
69
+ return nil unless format
70
+ name = format.to_sym
71
+ mime = get_mime_type(format)
72
+ formatter = to_format(name)
73
+ formatter[data] ? [formatter[data], mime] : nil
74
+ end
75
+
76
+ private
77
+
78
+ def handle(result, name, request, data)
79
+ if format = request.params[:format] || maker.format
80
+ response, mime = serialize(data, format)
81
+ request.content_type(mime) if mime
82
+ response or request.error(406)
83
+ else
84
+ request.instance_variable_set(ivar_name(data), data)
85
+ response = Response.new(maker, request)
86
+ response.instance_exec(data, &defaults[name][result])
87
+ end
88
+ end
89
+
90
+ def get_mime_type(format)
91
+ Rack::Mime::MIME_TYPES['.' + format.to_s]
92
+ end
93
+
94
+ def ivar_name(data)
95
+ "@" + (data.kind_of?(Array) ? model.plural : model.singular)
96
+ end
97
+
98
+ def to_format(name)
99
+ maker.formats[name] || Proc.new do |data|
100
+ method_name = "to_#{name}"
101
+ data.respond_to?(method_name) ? data.send(method_name) : nil
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,60 @@
1
+ module Sinatra
2
+ class NoTemplateError < StandardError; end
3
+
4
+ module Hat
5
+ # Tells Sinatra what to do next.
6
+ class Response
7
+ attr_reader :maker
8
+
9
+ delegate :model, :resource_path, :to => :maker
10
+
11
+ def initialize(maker, request)
12
+ @maker = maker
13
+ @request = request
14
+ end
15
+
16
+ # Now uses haml by default
17
+ def render(action, options={})
18
+ begin
19
+ options.each { |sym, value| @request.send(sym, value) }
20
+ @request.haml "#{maker.prefix}/#{action}.html".to_sym
21
+ rescue Errno::ENOENT
22
+ no_template! "Can't find #{File.expand_path(File.join(views, action.to_s))}.html.haml"
23
+ end
24
+ end
25
+
26
+ def redirect(*args)
27
+ @request.redirect url_for(*args)
28
+ end
29
+
30
+ def url_for(resource, *args)
31
+ case resource
32
+ when String then resource
33
+ when Symbol then resource_path(Maker.actions[resource][:path], *args)
34
+ else maker_for(resource).resource_path('/:id', resource)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def no_template!(msg)
41
+ raise NoTemplateError.new(msg)
42
+ end
43
+
44
+ def views
45
+ @views ||= begin
46
+ if views_dir = @request.options.views
47
+ File.join(views_dir, maker.prefix)
48
+ else
49
+ no_template! "Make sure you set the :views option!"
50
+ end
51
+ end
52
+ end
53
+
54
+ def maker_for(record)
55
+ resource = record.is_a?(model.klass) ? maker : maker.parents.detect { |m| record.is_a?(m.model.klass) }
56
+ resource || maker
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,46 @@
1
+ module Sinatra
2
+ module Hat
3
+ # Tells Sinatra which routes to generate. The routes
4
+ # created automatically when the actions are loaded.
5
+ class Router
6
+ delegate :resource_path, :logger, :to => :maker
7
+
8
+ attr_reader :maker, :app
9
+
10
+ def self.cache
11
+ @cache ||= []
12
+ end
13
+
14
+ def initialize(maker)
15
+ @maker = maker
16
+ end
17
+
18
+ def generate(app)
19
+ @app = app
20
+
21
+ Router.cache.each do |route|
22
+ map(*route)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def map(method, action, path)
29
+ path = resource_path(path)
30
+
31
+ handler = lambda do |request|
32
+ maker.handle(action, request)
33
+ end
34
+
35
+ logger.info ">> route for #{maker.klass} #{action}:\t#{method.to_s.upcase}\t#{path}"
36
+
37
+ app.send(method, "#{path}*" + "/?") do
38
+ id, format = (params[:id].to_s + params[:splat].join).split('.')
39
+ params['id'] = id
40
+ params['format'] = format
41
+ handler[self]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{sinatras-hat}
5
+ s.version = "0.1.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Pat Nakajima"]
9
+ s.date = %q{2009-01-17}
10
+ s.email = %q{patnakajima@gmail.com}
11
+ s.files = ["README.md", "lib/core_ext", "lib/core_ext/array.rb", "lib/core_ext/hash.rb", "lib/core_ext/module.rb", "lib/core_ext/object.rb", "lib/sinatras-hat", "lib/sinatras-hat/actions.rb", "lib/sinatras-hat/authentication.rb", "lib/sinatras-hat/extendor.rb", "lib/sinatras-hat/hash_mutator.rb", "lib/sinatras-hat/logger.rb", "lib/sinatras-hat/maker.rb", "lib/sinatras-hat/model.rb", "lib/sinatras-hat/resource.rb", "lib/sinatras-hat/responder.rb", "lib/sinatras-hat/response.rb", "lib/sinatras-hat/router.rb", "lib/sinatras-hat.rb"]
12
+ s.require_paths = ["lib"]
13
+ s.rubygems_version = %q{1.3.1}
14
+ s.summary = %q{Simple REST-ful resources with Sinatra.}
15
+
16
+ if s.respond_to? :specification_version then
17
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
18
+ s.specification_version = 2
19
+
20
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
21
+ s.add_runtime_dependency(%q<extlib>, [">= 0"])
22
+ s.add_runtime_dependency(%q<metaid>, [">= 0"])
23
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
24
+ else
25
+ s.add_dependency(%q<extlib>, [">= 0"])
26
+ s.add_dependency(%q<metaid>, [">= 0"])
27
+ s.add_dependency(%q<sinatra>, [">= 0"])
28
+ end
29
+ else
30
+ s.add_dependency(%q<extlib>, [">= 0"])
31
+ s.add_dependency(%q<metaid>, [">= 0"])
32
+ s.add_dependency(%q<sinatra>, [">= 0"])
33
+ end
34
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe "handle create" do
4
+ attr_reader :maker, :app, :request, :article
5
+
6
+ before(:each) do
7
+ build_models!
8
+ mock_app { }
9
+ @maker = new_maker(Article)
10
+ @request = fake_request("article[name]" => "The article")
11
+ stub(request).redirect(anything)
12
+ end
13
+
14
+ def handle(*args)
15
+ maker.handle(:create, *args)
16
+ end
17
+
18
+ it "instantiates a new record and saves it" do
19
+ mock.proxy(article = Article.new).save
20
+ mock.proxy(maker.model).new("article[name]" => "The article") { article }
21
+ handle(request)
22
+ end
23
+
24
+ describe "responding" do
25
+ attr_reader :new_article
26
+
27
+ before(:each) do
28
+ @new_article = Article.new
29
+ end
30
+
31
+ context "when the save is successful" do
32
+ before(:each) do
33
+ stub(Article).new(anything).returns(new_article)
34
+ end
35
+
36
+ context "when there's no format" do
37
+ it "redirects" do
38
+ mock(request).redirect(anything)
39
+ mock.proxy(maker.responder).success(:create, request, new_article)
40
+ handle(request)
41
+ end
42
+ end
43
+ end
44
+
45
+ context "when the save is not successful" do
46
+ before(:each) do
47
+ stub(Article).new(anything).returns(new_article)
48
+ stub(new_article).save { false }
49
+ end
50
+
51
+ context "when there's no format" do
52
+ it "renders edit template" do
53
+ mock(request).erb :"articles/new"
54
+ mock.proxy(maker.responder).failure(:create, request, new_article)
55
+ handle(request)
56
+ end
57
+ end
58
+
59
+ # context "when there is a format" do
60
+ # it "serializes the record" do
61
+ # request_with_format = fake_request(:format => "yaml")
62
+ # mock.proxy(maker.responder).serialize("yaml", article)
63
+ # handle(request_with_format)
64
+ # end
65
+ # end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe "handle destroy" do
4
+ attr_reader :maker, :app, :request, :article
5
+
6
+ before(:each) do
7
+ build_models!
8
+ mock_app { }
9
+ @maker = new_maker(Article)
10
+ @request = fake_request(:id => @article.to_param)
11
+ stub(maker.model).find(:id => @article.to_param) { @article }
12
+ stub(request).redirect(anything)
13
+ end
14
+
15
+ def handle(*args)
16
+ maker.handle(:destroy, *args)
17
+ end
18
+
19
+ it "takes a request" do
20
+ handle(request)
21
+ end
22
+
23
+ context "when the record exists" do
24
+ it "loads correct record" do
25
+ mock.proxy(maker.model).find(:id => @article.to_param) { article }
26
+ handle(request)
27
+ end
28
+
29
+ it "destroys the record" do
30
+ mock.proxy(article).destroy
31
+ handle(request)
32
+ end
33
+
34
+ describe "rendering a response" do
35
+ before(:each) do
36
+ params = { :id => @article.to_param }
37
+ @request = fake_request(params)
38
+ stub(maker.model).find(params).returns(article)
39
+ end
40
+
41
+ it "redirects to the index" do
42
+ mock(request).redirect('/articles')
43
+ handle(request)
44
+ end
45
+ end
46
+ end
47
+
48
+ context "when the record does not exist" do
49
+ before(:each) do
50
+ stub(maker.model).find(:id => @article.to_param) { nil }
51
+ end
52
+
53
+ it "returns not_found" do
54
+ mock.proxy(request).not_found
55
+ catch(:halt) { handle(request) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec/spec_helper'
2
+
3
+ describe "handle show" do
4
+ attr_reader :maker, :app, :request
5
+
6
+ before(:each) do
7
+ build_models!
8
+ mock_app { }
9
+ @maker = new_maker(Article)
10
+ @request = fake_request(:id => @article.to_param)
11
+ end
12
+
13
+ def handle(*args)
14
+ maker.handle(:edit, *args)
15
+ end
16
+
17
+ it "takes a request" do
18
+ handle(request)
19
+ end
20
+
21
+ describe "rendering not_found" do
22
+ before(:each) do
23
+ stub(maker.model).find(request.params).returns(nil)
24
+ stub(request).not_found # because it throws :halt otherwise
25
+ end
26
+
27
+ it "returns not_found" do
28
+ mock(request).not_found
29
+ handle(request)
30
+ end
31
+ end
32
+
33
+ describe "rendering a successful response" do
34
+ it "loads correct record" do
35
+ mock.proxy(maker.model).find(:id => @article.to_param) { :article }
36
+ handle(request)
37
+ end
38
+
39
+ context "when there's no :format param" do
40
+ before(:each) do
41
+ params = { :id => @article.to_param }
42
+ @request = fake_request(params)
43
+ stub(maker.model).find(params).returns(:article)
44
+ end
45
+
46
+ it "renders the show template" do
47
+ mock.proxy(maker.responder).success(:edit, request, :article)
48
+ handle(request)
49
+ end
50
+ end
51
+ end
52
+ end