From af728f6cfc35a9e6d87f79e62ff8d4c998f05ae9 Mon Sep 17 00:00:00 2001 From: "Gary S. Weaver" Date: Wed, 17 Apr 2013 18:04:33 -0400 Subject: [PATCH] version 3.4.0 --- Appraisals | 21 ++-- CHANGELOG.md | 10 +- Gemfile.lock | 2 +- README.md | 70 ++++++++++++ gemfiles/rails_3.1.12.gemfile | 2 +- gemfiles/rails_3.1.12.gemfile.lock | 2 +- gemfiles/rails_3.2.13.gemfile | 2 +- gemfiles/rails_3.2.13.gemfile.lock | 2 +- gemfiles/rails_4.0.0.beta1.gemfile.lock | 2 +- gemfiles/rails_rails_4.0.0.beta1.gemfile | 17 +++ lib/restful_json/config.rb | 19 +++- lib/restful_json/controller.rb | 88 +++++++++++++++ lib/restful_json/default_controller.rb | 6 + lib/restful_json/version.rb | 2 +- spec/controllers/foobars_controller_spec.rb | 18 +++ spec/controllers/posts_controller_spec.rb | 72 ++++++++++++ .../app/assets/javascripts/posts.js.coffee | 3 + .../app/assets/stylesheets/posts.css.scss | 3 + .../app/assets/stylesheets/scaffolds.css.scss | 69 ++++++++++++ .../app/controllers/my_posts_controller.rb | 3 + .../dummy/app/controllers/posts_controller.rb | 106 ++++++++++++++++++ .../app/models/able_to_fail_on_purpose.rb | 16 +++ spec/dummy/app/models/barfoo.rb | 1 + spec/dummy/app/models/foobar.rb | 1 + spec/dummy/app/models/my_post.rb | 3 + spec/dummy/app/models/post.rb | 3 + spec/dummy/app/permitters/post_permitter.rb | 3 + spec/dummy/app/views/posts/_form.html.erb | 29 +++++ spec/dummy/app/views/posts/edit.html.erb | 6 + spec/dummy/app/views/posts/index.html.erb | 31 +++++ .../dummy/app/views/posts/index.json.jbuilder | 4 + spec/dummy/app/views/posts/new.html.erb | 5 + spec/dummy/app/views/posts/show.html.erb | 20 ++++ spec/dummy/app/views/posts/show.json.jbuilder | 1 + .../config/initializers/session_store.rb | 5 +- .../config/initializers/wrap_parameters.rb | 25 +++-- spec/dummy/config/routes.rb | 2 + spec/dummy/db/schema.rb | 18 +++ 38 files changed, 661 insertions(+), 31 deletions(-) create mode 100644 gemfiles/rails_rails_4.0.0.beta1.gemfile create mode 100644 spec/controllers/posts_controller_spec.rb create mode 100644 spec/dummy/app/assets/javascripts/posts.js.coffee create mode 100644 spec/dummy/app/assets/stylesheets/posts.css.scss create mode 100644 spec/dummy/app/assets/stylesheets/scaffolds.css.scss create mode 100644 spec/dummy/app/controllers/my_posts_controller.rb create mode 100644 spec/dummy/app/controllers/posts_controller.rb create mode 100644 spec/dummy/app/models/able_to_fail_on_purpose.rb create mode 100644 spec/dummy/app/models/my_post.rb create mode 100644 spec/dummy/app/models/post.rb create mode 100644 spec/dummy/app/permitters/post_permitter.rb create mode 100644 spec/dummy/app/views/posts/_form.html.erb create mode 100644 spec/dummy/app/views/posts/edit.html.erb create mode 100644 spec/dummy/app/views/posts/index.html.erb create mode 100644 spec/dummy/app/views/posts/index.json.jbuilder create mode 100644 spec/dummy/app/views/posts/new.html.erb create mode 100644 spec/dummy/app/views/posts/show.html.erb create mode 100644 spec/dummy/app/views/posts/show.json.jbuilder diff --git a/Appraisals b/Appraisals index ea84d1c..144f730 100644 --- a/Appraisals +++ b/Appraisals @@ -1,16 +1,15 @@ -['4.0.0.beta1'].each do |rails_version| - appraise "rails_#{rails_version}" do - gem "rails", rails_version - gem 'json_spec' - #gem 'autolog' - end -end +# broken for now +#appraise 'rails-edge' do +# gem 'rails', :git => 'git://github.com/rails/rails.git' +# gem 'json_spec' +#end -['3.2.13', '3.1.12'].each do |rails_version| +['4.0.0.beta1', '3.2.13', '3.1.12'].each do |rails_version| appraise "rails_#{rails_version}" do - gem "rails", rails_version - gem 'strong_parameters' + gem 'rails', rails_version gem 'json_spec' - #gem 'autolog' + if rails_version.start_with?('3.') + gem 'strong_parameters' + end end end diff --git a/CHANGELOG.md b/CHANGELOG.md index 062bc6c..ab70cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ +## restful_json 3.4.0 ## + +* Added rescue_class config option to indicate highest level class to rescue for every action. (nil/NilClass indicates to re-raise StandardError.) +* Added rescue_handlers config option as substitute for having to rescue, log, set status and i18n message key for sets of exceptions. +* Added return_error_data config option to also return the exception's class.name, class.message, and class.backtrace (cleaned) in "error_data" in error response. + ## restful_json 3.3.4 ## -* debug mode for controller/app will now log debug to Rails logger if true -* head: ok with no body on destroy for no errors in formats other than HTML, like it did in versions before restful_json v3.3.0. +* If debug config option true, controller/app will now log debug to Rails logger. +* Fixing bug: will return head: ok with no body on destroy for no errors in formats other than HTML, like it did in versions before restful_json v3.3.0. ## restful_json 3.3.3 ## diff --git a/Gemfile.lock b/Gemfile.lock index 95f251a..3f31a82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - restful_json (3.3.4) + restful_json (3.4.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index b3aabb5..dfc6f95 100644 --- a/README.md +++ b/README.md @@ -701,6 +701,74 @@ You would get the response: For more realistic use that takes advantage of existing configuration in the controller, take a look at the controller in `lib/restful_json/controller.rb` to see how the actions are defined, and just copy/paste into your controller or module, etc. +### Error Handling + +#### Properly Handling Non-controller-action Errors + +Some things restful_json can't do in the controller, like responding with json for a json request when the route is not setup correctly or an action is missing. + +Rails 4 has basic error handling defined in the [public_exceptions][public_exceptions] and [show_exceptions][show_exceptions] Rack middleware. + +Rails 3.2.x has support for `config.exceptions_app` which can be defined as the following to simulate Rails 4 exception handling: + + config.exceptions_app = lambda do |env| + exception = env["action_dispatch.exception"] + status = env["PATH_INFO"][1..-1] + request = ActionDispatch::Request.new(env) + content_type = request.formats.first + body = { :status => status, :error => exception.message } + format = content_type && "to_#{content_type.to_sym}" + if format && body.respond_to?(format) + formatted_body = body.public_send(format) + [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}", + 'Content-Length' => body.bytesize.to_s}, [formatted_body]] + else + found = false + path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale + path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + + if found || File.exist?(path) + [status, {'Content-Type' => "text/html; charset=#{ActionDispatch::Response.default_charset}", + 'Content-Length' => body.bytesize.to_s}, [File.read(path)]] + else + [404, { "X-Cascade" => "pass" }, []] + end + end + end + +That is just a collapsed version of the behavior of [public_exceptions][public_exceptions] as of April 2013, pre-Rails 4.0.0, so please look at the latest version and adjust accordingly. Use at your own risk, obviously. + +Unfortunately, this doesn't work for Rails 3.1.x. However, in many scenarios there is the chance at a rare situation when the proper format is not returned to the client, even if everything is controlled as much as possible on the server. So, the client really needs to be able to handle such a case of unexpected format with a generic error. + +But, if you can make Rack respond a little better for some errors, that's great. + +#### Controller Error Handling Configuration + +The default configuration will rescue StandardError in each action method and will render as 404 for ActiveRecord::RecordNotFound or 500 for all other StandardError (and ancestors, like a normal rescue). + +There are a few options to customize the rescue and error rendering behavior. + +The `rescue_class` config option specifies what to rescue. Set to StandardError to behave like a normal rescue. Set to nil to just reraise everything rescued (to disable handling). + +The `rescue_handlers` config option is like a minimalist set of rescue blocks that apply to every action method. For example, the following would effectively `rescue => e` (rescuing `StandardError`) and then for `ActiveRecord::RecordNotFound`, it would uses response status `:not_found` (HTTP 404). Otherwise it uses status `:internal_server_error` (HTTP 500). In both cases the error message is `e.message`: + + self.rescue_class = StandardError + self.rescue_handlers = [ + {exception_classes: [ActiveRecord::RecordNotFound], status: :not_found}, + {status: :internal_server_error} + ] + +In a slightly more complicated case, this configuration would catch all exceptions raised with each actinon method that had `ActiveRecord::RecordNotFound` as an ancestor and use the error message defined by i18n key 'api.not_found'. All other exceptions would use status `:internal_server_error` (because it is a default, and doesn't have to be specified) but would use the error message defined by i18n key 'api.internal_server_error': + + self.rescue_class = Exception + self.rescue_handlers = [ + {exception_ancestor_classes: [ActiveRecord::RecordNotFound], status: :not_found, i18n_key: 'api.not_found'.freeze}, + {i18n_key: 'api.internal_server_error'.freeze} + ] + + +The `return_error_data` config option will not only return a response with `status` and `error` but also an `error_data` containing the `e.class.name`, `e.message`, and cleaned `e.backtrace`. + ### Release Notes #### restful_json v3.3 @@ -758,4 +826,6 @@ Copyright (c) 2013 Gary S. Weaver, released under the [MIT license][lic]. [ar]: http://api.rubyonrails.org/classes/ActiveRecord/Relation.html [rails-api]: https://github.com/rails-api/rails-api [railscast320]: http://railscasts.com/episodes/320-jbuilder +[public_exceptions]: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/public_exceptions.rb +[show_exceptions]: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/show_exceptions.rb [lic]: http://github.com/rubyservices/restful_json/blob/master/LICENSE diff --git a/gemfiles/rails_3.1.12.gemfile b/gemfiles/rails_3.1.12.gemfile index 3b25b7e..75f4894 100644 --- a/gemfiles/rails_3.1.12.gemfile +++ b/gemfiles/rails_3.1.12.gemfile @@ -12,7 +12,7 @@ gem "sqlite3", :platform=>[:ruby, :ruby_18, :ruby_19, :ruby_20, :mri, :mri_18, : gem "active_model_serializers" gem "cancan" gem "rails", "3.1.12" -gem "strong_parameters" gem "json_spec" +gem "strong_parameters" gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails_3.1.12.gemfile.lock b/gemfiles/rails_3.1.12.gemfile.lock index f8d5bd8..1c2629d 100644 --- a/gemfiles/rails_3.1.12.gemfile.lock +++ b/gemfiles/rails_3.1.12.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /Users/gary/github/restful_json specs: - restful_json (3.3.4) + restful_json (3.4.0) GEM remote: https://rubygems.org/ diff --git a/gemfiles/rails_3.2.13.gemfile b/gemfiles/rails_3.2.13.gemfile index f51f421..614cbc0 100644 --- a/gemfiles/rails_3.2.13.gemfile +++ b/gemfiles/rails_3.2.13.gemfile @@ -12,7 +12,7 @@ gem "sqlite3", :platform=>[:ruby, :ruby_18, :ruby_19, :ruby_20, :mri, :mri_18, : gem "active_model_serializers" gem "cancan" gem "rails", "3.2.13" -gem "strong_parameters" gem "json_spec" +gem "strong_parameters" gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails_3.2.13.gemfile.lock b/gemfiles/rails_3.2.13.gemfile.lock index 43c1d2f..0160916 100644 --- a/gemfiles/rails_3.2.13.gemfile.lock +++ b/gemfiles/rails_3.2.13.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /Users/gary/github/restful_json specs: - restful_json (3.3.4) + restful_json (3.4.0) GEM remote: https://rubygems.org/ diff --git a/gemfiles/rails_4.0.0.beta1.gemfile.lock b/gemfiles/rails_4.0.0.beta1.gemfile.lock index 60d90d7..1842dbc 100644 --- a/gemfiles/rails_4.0.0.beta1.gemfile.lock +++ b/gemfiles/rails_4.0.0.beta1.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: /Users/gary/github/restful_json specs: - restful_json (3.3.4) + restful_json (3.4.0) GEM remote: https://rubygems.org/ diff --git a/gemfiles/rails_rails_4.0.0.beta1.gemfile b/gemfiles/rails_rails_4.0.0.beta1.gemfile new file mode 100644 index 0000000..8be155f --- /dev/null +++ b/gemfiles/rails_rails_4.0.0.beta1.gemfile @@ -0,0 +1,17 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rspec" +gem "rspec-rails" +gem "appraisal" +gem "activerecord-jdbc-adapter", :platform=>:jruby +gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby +gem "jruby-openssl", :platform=>:jruby +gem "sqlite3", :platform=>[:ruby, :ruby_18, :ruby_19, :ruby_20, :mri, :mri_18, :mri_19, :mri_20, :rbx, :mswin, :mingw, :mingw_18, :mingw_19, :mingw_20] +gem "active_model_serializers" +gem "cancan" +gem "rails", "rails_4.0.0.beta1" +gem "json_spec" + +gemspec :path=>"../" \ No newline at end of file diff --git a/lib/restful_json/config.rb b/lib/restful_json/config.rb index 7614416..dde1c7a 100644 --- a/lib/restful_json/config.rb +++ b/lib/restful_json/config.rb @@ -9,7 +9,10 @@ module RestfulJson :return_resource, :render_enabled, :use_permitters, - :avoid_respond_with + :avoid_respond_with, + :return_error_data, + :rescue_class, + :rescue_handlers ] class << self @@ -22,12 +25,22 @@ def configure(&blk); class_eval(&blk); end RestfulJson.configure do self.can_filter_by_default_using = [:eq] self.debug = false - self.filter_split = ',' + self.filter_split = ','.freeze self.formats = :json, :html self.number_of_records_in_a_page = 15 - self.predicate_prefix = '!' + self.predicate_prefix = '!'.freeze self.return_resource = false self.render_enabled = true self.use_permitters = true self.avoid_respond_with = true + self.return_error_data = true + # Set to nil to reraise StandardError in rescue vs. calling render_error(e) to render using restful_json + self.rescue_class = StandardError + # Ordered array of handlers to handle rescue of self.rescue_class or sub types. Can use optional i18n_key for message, but will default to e.message if not found. + # Eventually may support [DataMapper::ObjectNotFoundError], [MongoMapper::DocumentNotFound], etc. + # If no exception_classes or exception_ancestor_classes provided, it will always match self.rescue_class. + self.rescue_handlers = [ + {exception_classes: [ActiveRecord::RecordNotFound], status: :not_found, i18n_key: 'api.not_found'.freeze}, + {status: :internal_server_error, i18n_key: 'api.internal_server_error'.freeze} + ] end diff --git a/lib/restful_json/controller.rb b/lib/restful_json/controller.rb index 4e3bc54..1dd1220 100644 --- a/lib/restful_json/controller.rb +++ b/lib/restful_json/controller.rb @@ -197,6 +197,80 @@ def convert_request_param_value_for_filtering(attr_sym, value) value && NILS.include?(value) ? nil : value end + def safe_i18n_t(i18n_key) + I18n.t(i18n_key) + rescue => e + logger.debug "failed to get i18n message for #{i18n_key.inspect}: #{e.message}\n#{e.backtrace.join('\n')}" if self.debug + end + + # Returns self.return_error_data by default. To only return error_data in dev and test, use this: + # `def enable_long_error?; Rails.env.development? || Rails.env.test?; end` + def include_error_data? + self.return_error_data + end + + # Searches through self.rescue_handlers for appropriate handler. + # self.rescue_handlers is an array of hashes where there is key :exception_classes and/or :exception_ancestor_classes + # along with :i18n_key and :status keys. + # :exception_classes contains an array of classes to exactly match the exception. + # :exception_ancestor_classes contains an array of classes that can match an ancestor of the exception. + # If exception handled, returns hash, hopefully containing keys :i18n_key and :status. + # Otherwise, returns nil which indicates that this exception should not be handled. + def exception_handling_data(e) + self.rescue_handlers.each do |handler| + return handler if (handler.key?(:exception_classes) && handler[:exception_classes].include?(e.class)) + if handler.key?(:exception_ancestor_classes) + handler[:exception_ancestor_classes].each do |ancestor| + return handler if e.class.ancestors.include?(ancestor) + end + elsif !handler.key?(:exception_classes) && !handler.key?(:exception_ancestor_classes) + return handler + end + end + nil + end + + def handle_or_raise(e) + raise e if self.rescue_class.nil? + handling_data = exception_handling_data(e) + raise e unless handling_data + # this is something we intended to rescue, so log it + logger.error(e) + # render error only if we haven't rendered response yet + render_error(e, handling_data) unless @performed_render + end + + # Renders error using handling data options (where options are probably hash from self.rescue_handlers that was matched). + # + # If include_error_data? is true, it returns something like the following (with the appropriate HTTP status code via setting appropriate status in respond_do: + # {"status": "not_found", + # "error": "Internationalized error message or e.message", + # "error_data": {"type": "ActiveRecord::RecordNotFound", "message": "Couldn't find Bar with id=23423423", "trace": ["backtrace line 1", ...]} + # } + # + # If include_error_data? is false, it returns something like: + # {"status": "not_found", "error", "Couldn't find Bar with id=23423423"} + # + # It handles any format in theory that is supported by respond_to and has a `to_(some format)` method. + def render_error(e, handling_data) + i18n_key = handling_data[:i18n_key] + msg = i18n_key ? safe_i18n_t(i18n_key) : e.message + status = handling_data[:status] || :internal_server_error + if include_error_data? + respond_to do |format| + format.html { render notice: msg } + format.any { render request.format.to_sym => {:status => status, error: msg, error_data: {type: e.class.name, message: e.message, trace: Rails.backtrace_cleaner.clean(e.backtrace)}}, status: status } + end + else + respond_to do |format| + format.html { render notice: msg } + format.any { render request.format.to_sym => {:status => status, error: msg}, status: status } + end + end + # return exception so we know it was handled + e + end + def render_or_respond(read_only_action, success_code = :ok) if self.render_enabled # 404/not found is just for update (not destroy, because idempotent destroy = no 404) @@ -341,6 +415,8 @@ def index @value = value instance_variable_set(@model_at_plural_name_sym, @value) render_or_respond(true) + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's show (get) method to return a resource. @@ -350,6 +426,8 @@ def show @value = @model_class.where(id: params[:id].to_s).first # don't raise exception if not found instance_variable_set(@model_at_singular_name_sym, @value) render_or_respond(true, @value.nil? ? :not_found : :ok) + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's new method (e.g. used for new record in html format). @@ -358,6 +436,8 @@ def new @value = @model_class.new instance_variable_set(@model_at_singular_name_sym, @value) render_or_respond(true) + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's edit method (e.g. used for edit record in html format). @@ -367,6 +447,8 @@ def edit @value = @model_class.where(id: params[:id].to_s).first! # raise exception if not found instance_variable_set(@model_at_singular_name_sym, @value) @value + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's create (post) method to create a resource. @@ -386,6 +468,8 @@ def create @value.save instance_variable_set(@model_at_singular_name_sym, @value) render_or_respond(false, :created) + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's update (put) method to update a resource. @@ -406,6 +490,8 @@ def update @value.update_attributes(allowed_params) unless @value.nil? instance_variable_set(@model_at_singular_name_sym, @value) render_or_respond(true, @value.nil? ? :not_found : :ok) + rescue self.rescue_class => e + handle_or_raise(e) end # The controller's destroy (delete) method to destroy a resource. @@ -423,6 +509,8 @@ def destroy else render_or_respond(false) end + rescue self.rescue_class => e + handle_or_raise(e) end end end diff --git a/lib/restful_json/default_controller.rb b/lib/restful_json/default_controller.rb index 69ec832..6a91cf7 100644 --- a/lib/restful_json/default_controller.rb +++ b/lib/restful_json/default_controller.rb @@ -7,6 +7,12 @@ module DefaultController include ::ActionController::StrongParameters include ::TwinTurbo::Controller include ::RestfulJson::Controller + + rescue_from Exception, :with => :render_error + rescue_from ActiveRecord::RecordNotFound, :with => :render_not_found + rescue_from ActionController::RoutingError, :with => :render_not_found + rescue_from ActionController::UnknownController, :with => :render_not_found + rescue_from AbstractController::ActionNotFound, :with => :render_not_found end end end diff --git a/lib/restful_json/version.rb b/lib/restful_json/version.rb index 610d6f6..1d11300 100644 --- a/lib/restful_json/version.rb +++ b/lib/restful_json/version.rb @@ -1,3 +1,3 @@ module RestfulJson - VERSION = '3.3.4' + VERSION = '3.4.0' end diff --git a/spec/controllers/foobars_controller_spec.rb b/spec/controllers/foobars_controller_spec.rb index 62f5891..39425ae 100644 --- a/spec/controllers/foobars_controller_spec.rb +++ b/spec/controllers/foobars_controller_spec.rb @@ -1,6 +1,9 @@ require 'rails' require 'spec_helper' +class SomeSubtypeOfStandardError < StandardError +end + describe FoobarsController do before(:each) do @@ -215,5 +218,20 @@ assert_match '', @response.body response.status.should eq(200), "destroy failed (got #{response.status}): #{response.body}" end + + it 'should fail with error if subclass of StandardError' do + FoobarsController.test_role = 'admin' + Foobar.delete_all + # won't wrap in test without this per https://github.com/rails/rails/issues/6633 + @request.env['CONTENT_TYPE'] = 'application/json' + b = Foobar.create(foo_id: SecureRandom.urlsafe_base64) + # expect this to make destroy fail and reset in after hook + $error_to_raise_on_next_save_or_destroy_only = SomeSubtypeOfStandardError.new("some type of standard error") + delete :destroy, id: b, format: :json + # this is a weak check + @response.body['error'].should_not be_nil + @response.body['some type of standard error'].should_not be_nil + response.status.should eq(500), "destroy should have failed with 500 (got #{response.status}): #{response.body}" + end end end diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb new file mode 100644 index 0000000..ad8ace0 --- /dev/null +++ b/spec/controllers/posts_controller_spec.rb @@ -0,0 +1,72 @@ +require 'rails' +require 'spec_helper' + +#[:json].each do |req_format| +# {PostsController => Post, MyPostsController => MyPost}.each do |controller_class, model_class| +# +# describe controller_class do +# +# before(:each) do +# @plural_model_class_sym = model_class.name.underscore.pluralize.to_sym +# @singular_model_class_sym = model_class.name.underscore.to_sym +# @value = model_class.create(name: 'MyString', title: 'MyString', content: 'MyText') +# end +# +# after(:each) do +# model_class.delete_all +# end +# +# it "should get index #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# get :index, format: req_format +# @response.status.should eq(200) +# assigns(@plural_model_class_sym).should_not be_nil, "#{controller_class.name} did not assign #{@plural_model_class_sym}" +# end +# +# if req_format == :html +# it "should get new #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# get :new, format: req_format +# @response.status.should eq(200), "#{controller_class.name} returned #{@response.status} when expected 200" +# end +# end +# +# it "should create post #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# initial_count = model_class.count +# @request.env['CONTENT_TYPE'] = 'application/json' if req_format == :json +# post :create, @singular_model_class_sym => { content: @value.content, name: @value.name, title: @value.title }, format: req_format +# model_class.count.should_not eq(initial_count), "#{model_class.name}.count should not have been #{initial_count}" +# if req_format == :html +# assert_redirected_to post_path(assigns(@singular_model_class_sym)), "was not redirected to #{post_path(assigns(@singular_model_class_sym))}" +# end +# end +# +# it "should show post #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# get :show, id: @value, format: req_format +# response.status.should eq(200), "#{controller_class.name} returned #{@response.status} when expected 200" +# end +# +# if req_format == :html +# it "should get edit #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# get :edit, id: @value, format: req_format +# response.status.should eq(200), "#{controller_class.name} returned #{@response.status} when expected 200" +# end +# end +# +# it "should update post #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# @request.env['CONTENT_TYPE'] = 'application/json' if req_format == :json +# patch :update, id: @value, @singular_model_class_sym => { content: @value.content, name: @value.name, title: @value.title }, format: req_format +# if req_format == :html +# assert_redirected_to post_path(assigns(@singular_model_class_sym)), "was not redirected to #{post_path(assigns(@singular_model_class_sym))}" +# end +# end +# +# it "should destroy post #{model_class == Post ? 'using default Rails scaffold\'s controller' : 'using restful_json controller'}" do +# initial_count = model_class.count +# delete :destroy, id: @value, format: req_format +# model_class.count.should eq(initial_count - 1), "#{model_class.name}.count was #{model_class.count} instead of #{initial_count - 1}" +# if req_format == :html +# assert_redirected_to posts_path, "was not redirected to #{posts_path}" +# end +# end +# end +# end +#end# \ No newline at end of file diff --git a/spec/dummy/app/assets/javascripts/posts.js.coffee b/spec/dummy/app/assets/javascripts/posts.js.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/spec/dummy/app/assets/javascripts/posts.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/spec/dummy/app/assets/stylesheets/posts.css.scss b/spec/dummy/app/assets/stylesheets/posts.css.scss new file mode 100644 index 0000000..ed4dfd1 --- /dev/null +++ b/spec/dummy/app/assets/stylesheets/posts.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Posts controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/spec/dummy/app/assets/stylesheets/scaffolds.css.scss b/spec/dummy/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 0000000..6ec6a8f --- /dev/null +++ b/spec/dummy/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,69 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + &:visited { + color: #666; + } + &:hover { + color: #fff; + background-color: #000; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/spec/dummy/app/controllers/my_posts_controller.rb b/spec/dummy/app/controllers/my_posts_controller.rb new file mode 100644 index 0000000..c8a429e --- /dev/null +++ b/spec/dummy/app/controllers/my_posts_controller.rb @@ -0,0 +1,3 @@ +class MyPostsController < ApplicationController + include RestfulJson::DefaultController +end \ No newline at end of file diff --git a/spec/dummy/app/controllers/posts_controller.rb b/spec/dummy/app/controllers/posts_controller.rb new file mode 100644 index 0000000..8a6aea1 --- /dev/null +++ b/spec/dummy/app/controllers/posts_controller.rb @@ -0,0 +1,106 @@ +class PostsController < ApplicationController + + # GET /posts + # GET /posts.json + def index + @posts = Post.all + end + + # GET /posts/1 + # GET /posts/1.json + def show + @post = Post.find(params[:id]) + end + + # GET /posts/new + def new + @post = Post.new + end + + # GET /posts/1/edit + def edit + @post = Post.find(params[:id]) + end + + # edge Rails comes with Jbuilder, which generates a different controller than 4.0.0.beta1 + if Object.const_defined?('Jbuilder') + + # POST <%= route_url %> + def create + @post = Post.new(post_params) + + if @post.saved? + redirect_to post, notice: 'Post was successfully created.' + else + render action: 'new' + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + @post = Post.find(params[:id]) + if @post.update(post_params) + redirect_to @post, notice: 'Post was successfully updated.' + else + render action: 'edit' + end + end + + # DELETE <%= route_url %>/1 + def destroy + @post = Post.find(params[:id]) + @post.destroy + redirect_to post_url, notice: 'Post was successfully destroyed.' + end + + else + + # POST /posts + # POST /posts.json + def create + @post = Post.new(post_params) + + respond_to do |format| + if @post.save + format.html { redirect_to @post, notice: 'Post was successfully created.' } + format.json { render action: 'show', status: :created, location: @post } + else + format.html { render action: 'new' } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /posts/1 + # PATCH/PUT /posts/1.json + def update + @post = Post.find(params[:id]) + respond_to do |format| + if @post.update(post_params) + format.html { redirect_to @post, notice: 'Post was successfully updated.' } + format.json { head :no_content } + else + format.html { render action: 'edit' } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /posts/1 + # DELETE /posts/1.json + def destroy + @post = Post.find(params[:id]) + @post.destroy + respond_to do |format| + format.html { redirect_to posts_url } + format.json { head :no_content } + end + end + end + + private + # Never trust parameters from the scary internet, only allow the white list through. + def post_params + params.require(:post).permit(:name, :title, :content) if Object.const_defined?('StrongParameters') + end +end \ No newline at end of file diff --git a/spec/dummy/app/models/able_to_fail_on_purpose.rb b/spec/dummy/app/models/able_to_fail_on_purpose.rb new file mode 100644 index 0000000..12a68ad --- /dev/null +++ b/spec/dummy/app/models/able_to_fail_on_purpose.rb @@ -0,0 +1,16 @@ +module AbleToFailOnPurpose + extend ActiveSupport::Concern + included do + before_save :possibly_fail + before_destroy :possibly_fail + end + + def possibly_fail + if $error_to_raise_on_next_save_or_destroy_only != nil + raiseable_obj = $error_to_raise_on_next_save_or_destroy_only + # reset so will only happen once + $error_to_raise_on_next_save_or_destroy_only = nil + raise raiseable_obj + end + end +end diff --git a/spec/dummy/app/models/barfoo.rb b/spec/dummy/app/models/barfoo.rb index 4bb67b9..522de73 100644 --- a/spec/dummy/app/models/barfoo.rb +++ b/spec/dummy/app/models/barfoo.rb @@ -1,2 +1,3 @@ class Barfoo < ActiveRecord::Base + include AbleToFailOnPurpose end diff --git a/spec/dummy/app/models/foobar.rb b/spec/dummy/app/models/foobar.rb index 11c47d3..c811729 100644 --- a/spec/dummy/app/models/foobar.rb +++ b/spec/dummy/app/models/foobar.rb @@ -1,2 +1,3 @@ class Foobar < ActiveRecord::Base + include AbleToFailOnPurpose end diff --git a/spec/dummy/app/models/my_post.rb b/spec/dummy/app/models/my_post.rb new file mode 100644 index 0000000..ca70dd5 --- /dev/null +++ b/spec/dummy/app/models/my_post.rb @@ -0,0 +1,3 @@ +class MyPost < ActiveRecord::Base + include AbleToFailOnPurpose +end \ No newline at end of file diff --git a/spec/dummy/app/models/post.rb b/spec/dummy/app/models/post.rb new file mode 100644 index 0000000..fea68d4 --- /dev/null +++ b/spec/dummy/app/models/post.rb @@ -0,0 +1,3 @@ +class Post < ActiveRecord::Base + include AbleToFailOnPurpose +end \ No newline at end of file diff --git a/spec/dummy/app/permitters/post_permitter.rb b/spec/dummy/app/permitters/post_permitter.rb new file mode 100644 index 0000000..3b9225f --- /dev/null +++ b/spec/dummy/app/permitters/post_permitter.rb @@ -0,0 +1,3 @@ +class MyPostPermitter < ApplicationPermitter + permit :name, :title, :content +end diff --git a/spec/dummy/app/views/posts/_form.html.erb b/spec/dummy/app/views/posts/_form.html.erb new file mode 100644 index 0000000..eddcce5 --- /dev/null +++ b/spec/dummy/app/views/posts/_form.html.erb @@ -0,0 +1,29 @@ +<%= form_for(@post) do |f| %> + <% if @post.errors.any? %> +
+

<%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:

+ + +
+ <% end %> + +
+ <%= f.label :name %>
+ <%= f.text_field :name %> +
+
+ <%= f.label :title %>
+ <%= f.text_field :title %> +
+
+ <%= f.label :content %>
+ <%= f.text_area :content %> +
+
+ <%= f.submit %> +
+<% end %> diff --git a/spec/dummy/app/views/posts/edit.html.erb b/spec/dummy/app/views/posts/edit.html.erb new file mode 100644 index 0000000..7205802 --- /dev/null +++ b/spec/dummy/app/views/posts/edit.html.erb @@ -0,0 +1,6 @@ +

Editing post

+ +<%= render 'form' %> + +<%= link_to 'Show', @post %> | +<%= link_to 'Back', posts_path %> diff --git a/spec/dummy/app/views/posts/index.html.erb b/spec/dummy/app/views/posts/index.html.erb new file mode 100644 index 0000000..4c129c7 --- /dev/null +++ b/spec/dummy/app/views/posts/index.html.erb @@ -0,0 +1,31 @@ +

Listing posts

+ + + + + + + + + + + + + + + <% @posts.each do |post| %> + + + + + + + + + <% end %> + +
NameTitleContent
<%= post.name %><%= post.title %><%= post.content %><%= link_to 'Show', post %><%= link_to 'Edit', edit_post_path(post) %><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%= link_to 'New Post', new_post_path %> diff --git a/spec/dummy/app/views/posts/index.json.jbuilder b/spec/dummy/app/views/posts/index.json.jbuilder new file mode 100644 index 0000000..576d885 --- /dev/null +++ b/spec/dummy/app/views/posts/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@posts) do |post| + json.extract! post, :name, :title, :content + json.url post_url(post, format: :json) +end \ No newline at end of file diff --git a/spec/dummy/app/views/posts/new.html.erb b/spec/dummy/app/views/posts/new.html.erb new file mode 100644 index 0000000..36ad742 --- /dev/null +++ b/spec/dummy/app/views/posts/new.html.erb @@ -0,0 +1,5 @@ +

New post

+ +<%= render 'form' %> + +<%= link_to 'Back', posts_path %> diff --git a/spec/dummy/app/views/posts/show.html.erb b/spec/dummy/app/views/posts/show.html.erb new file mode 100644 index 0000000..e314b19 --- /dev/null +++ b/spec/dummy/app/views/posts/show.html.erb @@ -0,0 +1,20 @@ +

<%= notice %>

+ +

+ Name: + <%= @post.name %> +

+ +

+ Title: + <%= @post.title %> +

+ +

+ Content: + <%= @post.content %> +

+ + +<%= link_to 'Edit', edit_post_path(@post) %> | +<%= link_to 'Back', posts_path %> diff --git a/spec/dummy/app/views/posts/show.json.jbuilder b/spec/dummy/app/views/posts/show.json.jbuilder new file mode 100644 index 0000000..a5834ab --- /dev/null +++ b/spec/dummy/app/views/posts/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @post, :name, :title, :content, :created_at, :updated_at diff --git a/spec/dummy/config/initializers/session_store.rb b/spec/dummy/config/initializers/session_store.rb index 26f1b16..10b5e3b 100644 --- a/spec/dummy/config/initializers/session_store.rb +++ b/spec/dummy/config/initializers/session_store.rb @@ -1,5 +1,8 @@ # Be sure to restart your server when you modify this file. +# although we could look at if Object.const_defined?('Jbuilder'), a better solution to do the right thing in this case +# between 4.0.0.beta1 and edge Rails which have same version but behave differently is to just try both and not fail. if Rails::VERSION::MAJOR > 3 - Dummy::Application.config.session_store :encrypted_cookie_store, key: '_Dummy_session' + begin; Dummy::Application.config.session_store :encrypted_cookie_store, key: '_Dummy_session'; rescue; end + begin; Dummy::Application.config.session_store :cookie_store; rescue; end end diff --git a/spec/dummy/config/initializers/wrap_parameters.rb b/spec/dummy/config/initializers/wrap_parameters.rb index 33725e9..29ac25a 100644 --- a/spec/dummy/config/initializers/wrap_parameters.rb +++ b/spec/dummy/config/initializers/wrap_parameters.rb @@ -3,12 +3,23 @@ # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. -# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +## Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +#ActiveSupport.on_load(:action_controller) do +# wrap_parameters format: [:json] if respond_to?(:wrap_parameters) +#end +# +## To enable root element in JSON for ActiveRecord objects. +#ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +#end +# +##ActiveModel::Serializers::JSON.include_root_in_json = true + ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end - -# To enable root element in JSON for ActiveRecord objects. -# ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true -# end + +# Disable root element in JSON by default. +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = true +end \ No newline at end of file diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 60f7ad6..2295b21 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -4,4 +4,6 @@ end resources :foobars + resources :posts + resources :my_posts end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index e316439..8c3f136 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -17,4 +17,22 @@ create_table :users do |t| t.string :role end + + # default Rails 4 beta1 scaffold generated model + create_table :posts do |t| + t.string :name + t.string :title + t.text :content + + t.timestamps + end + + # copied default Rails 4 scaffold-generated model for comparison + create_table :my_posts do |t| + t.string :name + t.string :title + t.text :content + + t.timestamps + end end