diff --git a/.gitignore b/.gitignore index dffd531e6..4f3b0d64b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ tmtags ## VIM *.swp -*.swo ## RUBYMINE .idea diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index 97646860c..8e1ad5bd2 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,24 +1,14 @@ -0.2.2 (Next Release) -==================== +Next Release +============ -Features --------- - -* [#201](https://github.com/intridea/grape/pull/201), [#236](https://github.com/intridea/grape/pull/236), [#221](https://github.com/intridea/grape/pull/221): Added coercion and validations support to `params` DSL - [@schmurfy](https://github.com/schmurfy), [@tim-vandecasteele](https://github.com/tim-vandecasteele), [@adamgotterer](https://github.com/adamgotterer). -* [#204](https://github.com/intridea/grape/pull/204): Added ability to declare shared `params` at `namespace` level - [@tim-vandecasteele](https://github.com/tim-vandecasteele). -* [#234](https://github.com/intridea/grape/pull/234): Added a DSL for creating entities via mixin - [@mbleigh](https://github.com/mbleigh). -* [#240](https://github.com/intridea/grape/pull/240): Define API response format from a query string `format` parameter, if specified - [@neetiraj](https://github.com/neetiraj). - -Fixes ------ - -* [#248](https://github.com/intridea/grape/pull/248): Fix: API `version` returns last version set - [@narkoz](https://github.com/narkoz). -* [#242](https://github.com/intridea/grape/issues/242): Fix: permanent redirect status should be `301`, was `304` - [@adamgotterer](https://github.com/adamgotterer). -* [#211](https://github.com/intridea/grape/pull/211): Fix: custom validations are no longer triggered when optional and parameter is not present - [@adamgotterer](https://github.com/adamgotterer). +* [#201](https://github.com/intridea/grape/pull/201): Added custom exceptions to Grape. Updated validations to use ValidationError that can be rescued. - [@adamgotterer](https://github.com/adamgotterer). +* [#211](https://github.com/intridea/grape/pull/211): Updates to validation and coercion: Fix #211 and force order of operations for presence and coercion - [@adamgotterer](https://github.com/adamgotterer). * [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer). +* [#201](https://github.com/intridea/grape/pull/201): Rewritten `params` DSL, including support for coercion and validations - [@schmurfy](https://github.com/schmurfy). * [#205](https://github.com/intridea/grape/pull/205): Fix: Corrected parsing of empty JSON body on POST/PUT - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#181](https://github.com/intridea/grape/pull/181): Fix: Corrected JSON serialization of nested hashes containing `Grape::Entity` instances - [@benrosenblum](https://github.com/benrosenblum). * [#203](https://github.com/intridea/grape/pull/203): Added a check to `Entity#serializable_hash` that verifies an entity exists on an object - [@adamgotterer](https://github.com/adamgotterer). +* [#204](https://github.com/intridea/grape/pull/204): Added ability to declare shared parameters at namespace level - [@tim-vandecasteele](https://github.com/tim-vandecasteele). * [#208](https://github.com/intridea/grape/pull/208): `Entity#serializable_hash` must also check if attribute is generated by a user supplied block - [@ppadron](https://github.com/ppadron). 0.2.1 (7/11/2012) diff --git a/README.markdown b/README.markdown index d4ad3b6e3..dfc83493a 100644 --- a/README.markdown +++ b/README.markdown @@ -2,11 +2,11 @@ ## What is Grape? -Grape is a REST-like API micro-framework for Ruby. It's designed to run on Rack -or complement existing web application frameworks such as Rails and Sinatra by -providing a simple DSL to easily develop RESTful APIs. It has built-in support -for common conventions, including multiple formats, subdomain/prefix restriction, -content negotiation, versioning and much more. +Grape is a REST-like API micro-framework for Ruby. It's built to complement +existing web application frameworks such as Rails and Sinatra by providing a +simple DSL to easily develop RESTful APIs. It has built-in support for common +conventions, including multiple formats, subdomain/prefix restriction, content +negotiation, versioning and much more. [![Build Status](http://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape) @@ -41,7 +41,6 @@ the context of recreating parts of the Twitter API. ``` ruby class Twitter::API < Grape::API version 'v1', :using => :header, :vendor => 'twitter' - format :json helpers do def current_user @@ -215,17 +214,12 @@ end ## Parameter Validation and Coercion -You can define validations and coercion options for your parameters using a `params` block. +You can define validations and coercion options for your parameters using `params`. ```ruby params do requires :id, type: Integer optional :name, type: String, regexp: /^[a-z]+$/ - - group :user do - requires :first_name - requires :last_name - end end get ':id' do # params[:id] is an Integer @@ -235,9 +229,6 @@ end When a type is specified an implicit validation is done after the coercion to ensure the output type is the one declared. -Parameters can be nested using `group`. In the above example, this means both -`params[:user][:first_name]` and `params[:user][:last_name]` are required next to `params[:id]`. - ### Namespace Validation and Coercion Namespaces allow parameter definitions and apply to every method within the namespace. @@ -261,10 +252,10 @@ end ### Custom Validators ```ruby -class AlphaNumeric < Grape::Validations::Validator +class doit < Grape::Validations::Validator def validate_param!(attr_name, params) - unless params[attr_name] =~ /^[[:alnum:]]+$/ - throw :error, :status => 400, :message => "#{attr_name}: must consist of alpha-numeric characters" + unless params[attr_name] == 'im custom' + throw :error, :status => 400, :message => "#{attr_name}: is not custom!" end end end @@ -272,12 +263,11 @@ end ```ruby params do - requires :username, :alpha_numeric => true + requires :name, :doit => true end ``` -You can also create custom classes that take parameters. - +You can also create custom classes that take additional parameters ```ruby class Length < Grape::Validations::SingleOptionValidator def validate_param!(attr_name, params) @@ -294,21 +284,7 @@ params do end ``` -### Validation Errors - -When validation and coercion erros occur an exception of type `ValidationError` is raised. -If the exception goes uncaught it will respond with a status of 400 and an error message. -You can rescue a `ValidationError` and respond with a custom response. -```ruby -rescue_from ValidationError do |e| - Rack::Response.new({ - 'status' => e.status, - 'message' => e.message, - 'param' => e.param - }.to_json, e.status) -end -``` ## Headers @@ -342,7 +318,7 @@ end ## Helpers You can define helper methods that your endpoints can use with the `helpers` -macro by either giving a block or a module. +macro by either giving a block or a module: ``` ruby module MyHelpers @@ -371,7 +347,7 @@ end ## Cookies -You can set, get and delete your cookies very simply using `cookies` method. +You can set, get and delete your cookies very simply using `cookies` method: ``` ruby class API < Grape::API @@ -387,7 +363,7 @@ class API < Grape::API end ``` -To set more than value use hash-based syntax. +To set more than value use hash-based syntax: ``` ruby cookies[:counter] = { @@ -401,7 +377,7 @@ cookies[:counter][:value] +=1 ## Redirecting -You can redirect to a new url temporarily (302) or permanently (301). +You can redirect to a new url temporarily or permanently. ``` ruby redirect "/new_url" @@ -429,7 +405,7 @@ error!({ "error" => "unexpected error", "detail" => "missing widget" }, 500) ## Exception Handling Grape can be told to rescue all exceptions and instead return them in -txt or json formats. +text or json formats. ``` ruby class Twitter::API < Grape::API @@ -540,7 +516,7 @@ By default, Grape supports _XML_, _JSON_, _Atom_, _RSS_, and _text_ content-type Serialization takes place automatically. Your API can declare additional types to support. Response format is determined by the -request's extension, an explicit `format` parameter in the query string, or `Accept` header. +request's extension or `Accept` header. ``` ruby class Twitter::API < Grape::API @@ -551,8 +527,7 @@ end You can also set the default format. The order for choosing the format is the following. * Use the file extension, if specified. If the file is .json, choose the JSON format. -* Use the value of the `format` parameter in the query string, if specified. -* Use the format set by the `format` option, if specified. +* Use the format, if specified by the `format` option. * Attempt to find an acceptable format from the `Accept` header. * Use the default format, if specified by the `default_format` option. * Default to `:txt` otherwise. @@ -585,22 +560,22 @@ ever larger responses, using inheritance. Entities inherit from Grape::Entity, and define a simple DSL. Exposures can use runtime options to determine which fields should be visible, these options are -available to `:if`, `:unless`, and `:proc`. The option keys `:version` and `:collection` -will always be defined. The `:version` key is defined as `api.version`. The -`:collection` key is boolean, and defined as `true` if the object presented is an +available to :if, :unless, and :proc. The option keys :version and :collection +will always be defined. The :version key is defined as api.version. The +:collection key is boolean, and defined as true if the object presented is an array. * `expose SYMBOLS` * define a list of fields which will always be exposed * `expose SYMBOLS, HASH` - * HASH keys include `:if`, `:unless`, `:proc`, `:as`, `:using`, `:format_with`, `:documentation` - * `:if` and `:unless` accept hashes (passed during runtime) or procs (arguments are object and options) - * `expose SYMBOL, { :format_with => :formatter }` + * HASH keys include :if, :unless, :proc, :as, :using, :format_with, :documentation + * :if and :unless accept hashes (passed during runtime) or procs (arguments are object and options) + * `expose SYMBOL, {:format_with => :formatter}` * expose a value, formatting it first - * `:format_with` can only be applied to one exposure at a time - * `expose SYMBOL, { :as => "alias" }` + * :format_with can only be applied to one exposure at a time + * `expose SYMBOL, {:as => "alias"}` * Expose a value, changing its hash key from SYMBOL to alias - * `:as` can only be applied to one exposure at a time + * :as can only be applied to one exposure at a time * `expose SYMBOL BLOCK` * block arguments are object and options * expose the value returned by the block @@ -611,10 +586,10 @@ module API module Entities class User < Grape::Entity expose :first_name, :last_name - expose :field, :documentation => { :type => "string", :desc => "words go here" } - expose :email, :if => { :type => :full } - expose :user_type, user_id, :if => lambda{ |user,options| user.confirmed? } - expose(:name) { |user,options| [ user.first_name, user.last_name ].join(' ')} + expose :field, :documentation => {:type => "string", :desc => "words go here"} + expose :email, :if => {:type => :full} + expose :user_type, user_id, :if => lambda{|user,options| user.confirmed?} + expose(:name){|user,options| [user.first_name, user.last_name].join(' ')} expose :latest_status, :using => API::Status, :as => :status end end @@ -629,32 +604,11 @@ module API end ``` -#### Using the Exposure DSL - -Grape ships with a DSL to easily define entities within the context -of an existing class: - -```ruby -class User - include Grape::Entity::DSL - - entity :name, :email do - expose :advanced, if: :conditional - end -end -``` - -The above will automatically create a `User::Entity` class and -define properties on it according to the same rules as above. If -you only want to define simple exposures you don't have to supply -a block and can instead simply supply a list of comma-separated -symbols. - ### Using Entities -Once an entity is defined, it can be used within endpoints, by calling `present`. The `present` +Once an entity is defined, it can be used within endpoints, by calling #present. The #present method accepts two arguments, the object to be presented and the options associated with it. The -options hash must always include `:with`, which defines the entity to expose. +options hash must always include :with, which defines the entity to expose. If the entity includes documentation it can be included in an endpoint's description. @@ -675,58 +629,32 @@ module API end ``` -### Entity Organization - -In addition to separately organizing entities, it may be useful to -put them as namespaced classes underneath the model they represent. -For example: - -```ruby -class User - def entity - Entity.new(self) - end - - class Entity < Grape::Entity - expose :name, :email - end -end -``` - -If you organize your entities this way, Grape will automatically -detect the `Entity` class and use it to present your models. In -this example, if you added `present User.new` to your endpoint, -Grape would automatically detect that there is a `User::Entity` -class and use that as the representative entity. This can still -be overridden by using the `:with` option or an explicit -`represents` call. - ### Caveats Entities with duplicate exposure names and conditions will silently overwrite one another. -In the following example, when `object.check` equals "foo", only `field_a` will be exposed. -However, when `object.check` equals "bar" both `field_b` and `foo` will be exposed. +In the following example, when object#check equals "foo", only afield will be exposed. +However, when object#check equals "bar" both bfield and foo will be exposed. ```ruby module API module Entities class User < Grape::Entity - expose :field_a, :foo, :if => lambda { |object, options| object.check == "foo" } - expose :field_b, :foo, :if => lambda { |object, options| object.check == "bar" } + expose :afield, :foo, :if => lambda{|object,options| object.check=="foo"} + expose :bfield, :foo, :if => lambda{|object,options| object.check=="bar"} end end end ``` -This can be problematic, when you have mixed collections. Using `respond_to?` is safer. +This can be problematic, when you have mixed collections. Using #respond_to? is safer. ```ruby module API module Entities class User < Grape::Entity - expose :field_a, :if => lambda { |object, options| object.check == "foo" } - expose :field_b, :if => lambda { |object, options| object.check == "bar" } - expose :foo, :if => lambda { |object, options| object.respond_to?(:foo) } + expose :afield, :if => lambda{|object,options| object.check=="foo"} + expose :bfield, :if => lambda{|object,options| object.check=="bar"} + expose :foo, :if => lambda{object,options| object.respond_to?(:foo)} end end end @@ -865,16 +793,13 @@ RSpec.configure do |config| end ``` -## Contributing to Grape - -Grape is work of dozens of contributors. You're encouraged to submit pull requests, propose -features and discuss issues. +## Note on Patches/Pull Requests * Fork the project * Write tests for your new feature or a test that reproduces a bug * Implement your feature or make a bug fix * Do not mess with Rakefile, version or history -* Commit, push and make a pull request. Bonus points for topic branches. +* Commit, push and make a pull request. Bonus points for topical branches. ## License @@ -882,4 +807,5 @@ MIT License. See LICENSE for details. ## Copyright -Copyright (c) 2010-2012 Michael Bleigh, and Intridea, Inc. +Copyright (c) 2010-2012 Michael Bleigh and Intridea, Inc. + diff --git a/grape.gemspec b/grape.gemspec index 4e32366da..c469f468c 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -17,9 +17,9 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rack' s.add_runtime_dependency 'rack-mount' # s.add_runtime_dependency 'rack-jsonp' - s.add_runtime_dependency 'multi_json', '>= 1.3.2' + s.add_runtime_dependency 'multi_json' s.add_runtime_dependency 'multi_xml' - s.add_runtime_dependency 'hashie', '~> 1.2' + s.add_runtime_dependency 'hashie' s.add_runtime_dependency 'virtus' s.add_development_dependency 'rake' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index 2b7a5e501..17d55f7ac 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -10,7 +10,7 @@ module Grape # class in order to build an API. class API extend Validations::ClassMethods - + class << self attr_reader :route_set attr_reader :versions @@ -28,7 +28,7 @@ def logger(logger = nil) @logger ||= Logger.new($stdout) end end - + def reset! @settings = Grape::Util::HashStack.new @route_set = Rack::Mount::RouteSet.new @@ -106,8 +106,6 @@ def version(*args, &block) set(:version_options, options) end end - - @versions.last unless @versions.nil? end # Add a description to the next namespace or function. @@ -126,7 +124,7 @@ def default_format(new_format = nil) def format(new_format = nil) new_format ? set(:format, new_format.to_sym) : settings[:format] end - + # Specify the format for error messages. # May be `:json` or `:txt` (default). def error_format(new_format = nil) @@ -293,7 +291,7 @@ def route(methods, paths = ['/'], route_options = {}, &block) :route_options => (@namespace_description || {}).deep_merge(@last_description || {}).deep_merge(route_options || {}) } endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block) - + @last_description = nil reset_validations! end @@ -302,10 +300,6 @@ def before(&block) imbue(:befores, [block]) end - def after_validation(&block) - imbue(:after_validations, [block]) - end - def after(&block) imbue(:afters, [block]) end diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 071f97593..535cf4639 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -159,7 +159,7 @@ def error!(message, status=403) def redirect(url, options = {}) merged_options = {:permanent => false }.merge(options) if merged_options[:permanent] - status 301 + status 304 else if env['HTTP_VERSION'] == 'HTTP/1.1' && request.request_method.to_s.upcase != "GET" status 303 @@ -255,8 +255,6 @@ def present(object, options = {}) entity_class ||= (settings[:representations] || {})[potential] end - entity_class ||= object.class.const_get(:Entity) if object.class.const_defined?(:Entity) - root = options.delete(:root) representation = if entity_class @@ -293,14 +291,11 @@ def run(env) self.extend helpers cookies.read(@request) - run_filters befores - Array(settings[:validations]).each do |validator| validator.validate!(params) end - run_filters after_validations - + run_filters befores response_text = instance_eval &self.block run_filters afters cookies.write(header) @@ -374,7 +369,6 @@ def run_filters(filters) end def befores; aggregate_setting(:befores) end - def after_validations; aggregate_setting(:after_validations) end def afters; aggregate_setting(:afters) end end end diff --git a/lib/grape/entity.rb b/lib/grape/entity.rb index beea18f4b..ee114cedc 100644 --- a/lib/grape/entity.rb +++ b/lib/grape/entity.rb @@ -43,60 +43,6 @@ module Grape class Entity attr_reader :object, :options - # The Entity DSL allows you to mix entity functionality into - # your existing classes. - module DSL - def self.included(base) - base.extend ClassMethods - ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)} - base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity) - end - - module ClassMethods - # Returns the automatically-created entity class for this - # Class. - def entity_class(search_ancestors=true) - klass = const_get(:Entity) if const_defined?(:Entity) - klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors - klass - end - - # Call this to make exposures to the entity for this Class. - # Can be called with symbols for the attributes to expose, - # a block that yields the full Entity DSL (See Grape::Entity), - # or both. - # - # @example Symbols only. - # - # class User - # include Grape::Entity::DSL - # - # entity :name, :email - # end - # - # @example Mixed. - # - # class User - # include Grape::Entity::DSL - # - # entity :name, :email do - # expose :latest_status, using: Status::Entity, if: :include_status - # expose :new_attribute, :if => {:version => 'v2'} - # end - # end - def entity(*exposures, &block) - entity_class.expose *exposures if exposures.any? - entity_class.class_eval(&block) if block_given? - entity_class - end - end - - # Instantiates an entity version of this object. - def entity - self.class.entity_class.new(self) - end - end - # This method is the primary means by which you will declare what attributes # should be exposed by the entity. # @@ -313,7 +259,7 @@ def serializable_hash(runtime_options = {}) return nil if object.nil? opts = options.merge(runtime_options || {}) exposures.inject({}) do |output, (attribute, exposure_options)| - if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts) + if exposure_options.has_key?(:proc) || object.respond_to?(attribute) && conditions_met?(exposure_options, opts) partial_output = value_for(attribute, opts) output[key_for(attribute)] = if partial_output.respond_to? :serializable_hash @@ -342,10 +288,7 @@ def value_for(attribute, options = {}) if exposure_options[:proc] exposure_options[:proc].call(object, options) elsif exposure_options[:using] - using_options = options.dup - using_options.delete(:collection) - using_options[:root] = nil - exposure_options[:using].represent(object.send(attribute), using_options) + exposure_options[:using].represent(object.send(attribute), :root => nil) elsif exposure_options[:format_with] format_with = exposure_options[:format_with] diff --git a/lib/grape/exceptions/validation_error.rb b/lib/grape/exceptions/validation_error.rb index 9a07b17bd..ce9fad9df 100644 --- a/lib/grape/exceptions/validation_error.rb +++ b/lib/grape/exceptions/validation_error.rb @@ -1,10 +1,4 @@ require 'grape/exceptions/base' class ValidationError < Grape::Exceptions::Base - attr_accessor :param - - def initialize(args = {}) - @param = args[:param].to_s if args.has_key? :param - super - end end diff --git a/lib/grape/middleware/formatter.rb b/lib/grape/middleware/formatter.rb index 858479976..03aeda166 100644 --- a/lib/grape/middleware/formatter.rb +++ b/lib/grape/middleware/formatter.rb @@ -19,7 +19,7 @@ def headers end def before - fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format] + fmt = format_from_extension || options[:format] || format_from_header || options[:default_format] if content_types.key?(fmt) if !env['rack.input'].nil? and (body = env['rack.input'].read).strip.length != 0 parser = parser_for fmt @@ -50,11 +50,6 @@ def format_from_extension nil end - def format_from_params - fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"] - fmt ? fmt.to_sym : nil - end - def format_from_header mime_array.each do |t| if mime_types.key?(t) diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb index 687c4fa49..6ae6ec263 100644 --- a/lib/grape/validations.rb +++ b/lib/grape/validations.rb @@ -8,10 +8,9 @@ module Validations # All validators must inherit from this class. # class Validator - def initialize(attrs, options, required, scope) + def initialize(attrs, options, required) @attrs = Array(attrs) @required = required - @scope = scope if options.is_a?(Hash) && !options.empty? raise "unknown options: #{options.keys}" @@ -19,8 +18,6 @@ def initialize(attrs, options, required, scope) end def validate!(params) - params = @scope.params(params) - @attrs.each do |attr_name| if @required || params.has_key?(attr_name) validate_param!(attr_name, params) @@ -43,7 +40,7 @@ def self.convert_to_short_name(klass) ## # Base class for all validators taking only one param. class SingleOptionValidator < Validator - def initialize(attrs, options, required, scope) + def initialize(attrs, options, required) @option = options super end @@ -70,11 +67,7 @@ def self.register_validator(short_name, klass) end class ParamsScope - attr_accessor :element, :parent - - def initialize(api, element, parent, &block) - @element = element - @parent = parent + def initialize(api, &block) @api = api instance_eval(&block) end @@ -96,22 +89,7 @@ def optional(*attrs) validates(attrs, validations) end - - def group(element, &block) - scope = ParamsScope.new(@api, element, self, &block) - end - - def params(params) - params = @parent.params(params) if @parent - params = params[@element] || {} if @element - params - end - - def full_name(name) - return "#{@parent.full_name(@element)}[#{name}]" if @parent - name.to_s - end - + private def validates(attrs, validations) doc_attrs = { :required => validations.keys.include?(:presence) } @@ -128,10 +106,9 @@ def validates(attrs, validations) if desc = validations.delete(:desc) doc_attrs[:desc] = desc end - - full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} } - @api.document_attribute(full_attrs, doc_attrs) - + + @api.document_attribute(attrs, doc_attrs) + # Validate for presence before any other validators if validations.has_key?(:presence) && validations[:presence] validate('presence', validations[:presence], attrs, doc_attrs) @@ -153,7 +130,7 @@ def validates(attrs, validations) def validate(type, options, attrs, doc_attrs) validator_class = Validations::validators[type.to_s] if validator_class - @api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required], self) + @api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required]) else raise "unknown validator: #{type}" end @@ -168,16 +145,17 @@ def reset_validations! end def params(&block) - ParamsScope.new(self, nil, nil, &block) + ParamsScope.new(self, &block) end def document_attribute(names, opts) - @last_description ||= {} - @last_description[:params] ||= {} - - Array(names).each do |name| - @last_description[:params][name[:name].to_s] ||= {} - @last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]}) + if @last_description + @last_description[:params] ||= {} + + Array(names).each do |name| + @last_description[:params][name.to_s] ||= {} + @last_description[:params][name.to_s].merge!(opts) + end end end diff --git a/lib/grape/validations/coerce.rb b/lib/grape/validations/coerce.rb index 7228c3e25..7b4e2e956 100644 --- a/lib/grape/validations/coerce.rb +++ b/lib/grape/validations/coerce.rb @@ -12,7 +12,7 @@ def validate_param!(attr_name, params) if valid_type?(new_value) params[attr_name] = new_value else - raise ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}" + raise ValidationError, :status => 400, :message => "invalid parameter: #{attr_name}" end end diff --git a/lib/grape/validations/presence.rb b/lib/grape/validations/presence.rb index 5242a393c..1fd9574d8 100644 --- a/lib/grape/validations/presence.rb +++ b/lib/grape/validations/presence.rb @@ -3,7 +3,7 @@ module Validations class PresenceValidator < Validator def validate_param!(attr_name, params) unless params.has_key?(attr_name) - raise ValidationError, :status => 400, :param => attr_name, :message => "missing parameter: #{attr_name}" + raise ValidationError, :status => 400, :message => "missing parameter: #{attr_name}" end end end diff --git a/lib/grape/validations/regexp.rb b/lib/grape/validations/regexp.rb index 0c325c03a..e5c488486 100644 --- a/lib/grape/validations/regexp.rb +++ b/lib/grape/validations/regexp.rb @@ -4,7 +4,7 @@ module Validations class RegexpValidator < SingleOptionValidator def validate_param!(attr_name, params) if params[attr_name] && !( params[attr_name].to_s =~ @option ) - raise ValidationError, :status => 400, :param => attr_name, :message => "invalid parameter: #{attr_name}" + raise ValidationError, :status => 400, :message => "invalid parameter: #{attr_name}" end end end diff --git a/spec/grape/api_spec.rb b/spec/grape/api_spec.rb index 03aecedb1..afb08cc85 100644 --- a/spec/grape/api_spec.rb +++ b/spec/grape/api_spec.rb @@ -21,21 +21,6 @@ def app; subject end end end - describe '.version' do - context 'when defined' do - it 'should return version value' do - subject.version 'v1' - subject.version.should == 'v1' - end - end - - context 'when not defined' do - it 'should return nil' do - subject.version.should be_nil - end - end - end - describe '.version using path' do it_should_behave_like 'versioning' do let(:macro_options) do @@ -329,20 +314,6 @@ def app; subject end last_response.body.should eql 'first second' end - it 'should add a after_validation filter' do - subject.after_validation { @foo = "first #{params[:id]}:#{params[:id].class}" } - subject.after_validation { @bar = 'second' } - subject.params do - requires :id, :type => Integer - end - subject.get '/' do - "#{@foo} #{@bar}" - end - - get '/', :id => "32" - last_response.body.should eql 'first 32:Fixnum second' - end - it 'should add a after filter' do m = double('after mock') subject.after { m.do_something! } @@ -702,7 +673,7 @@ def three it 'should not re-raise exceptions of type Grape::Exception::Base' do class CustomError < Grape::Exceptions::Base; end subject.get('/custom_exception'){ raise CustomError } - + lambda{ get '/custom_exception' }.should_not raise_error end @@ -926,9 +897,6 @@ class CommunicationError < RuntimeError; end end end end - it "should return the latest version set" do - subject.version.should == 'v2' - end it "should return versions" do subject.versions.should == [ 'v1', 'v2' ] end @@ -1061,7 +1029,7 @@ class CommunicationError < RuntimeError; end subject.routes.map { |route| { :description => route.route_description, :params => route.route_params } }.should eq [ - { :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter", :full_name=>"ns_param" }, "method_param" => { :required => false, :desc => "method parameter", :full_name=>"method_param" } } } + { :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter" }, "method_param" => { :required => false, :desc => "method parameter" } } } ] end it "should merge the parameters of nested namespaces" do @@ -1087,33 +1055,7 @@ class CommunicationError < RuntimeError; end subject.routes.map { |route| { :description => route.route_description, :params => route.route_params } }.should eq [ - { :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2", :full_name=>"ns_param" }, "ns1_param" => { :required => true, :desc => "ns1 param", :full_name=>"ns1_param" }, "ns2_param" => { :required => true, :desc => "ns2 param", :full_name=>"ns2_param" }, "method_param" => { :required => false, :desc => "method param", :full_name=>"method_param" } } } - ] - end - it "should provide a full_name for parameters in nested groups" do - subject.desc "nesting" - subject.params do - requires :root_param, :desc => "root param" - group :nested do - requires :nested_param, :desc => "nested param" - end - end - subject.get "method" do ; end - subject.routes.map { |route| - { :description => route.route_description, :params => route.route_params } - }.should eq [ - { :description => "nesting", :params => { "root_param" => { :required => true, :desc => "root param", :full_name=>"root_param" }, "nested_param" => { :required => true, :desc => "nested param", :full_name=>"nested[nested_param]" } } } - ] - end - it "should parse parameters when no description is given" do - subject.params do - requires :one_param, :desc => "one param" - end - subject.get "method" do ; end - subject.routes.map { |route| - { :description => route.route_description, :params => route.route_params } - }.should eq [ - { :description => nil, :params => { "one_param" => { :required => true, :desc => "one param", :full_name=>"one_param" } } } + { :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2" }, "ns1_param" => { :required => true, :desc => "ns1 param" }, "ns2_param" => { :required => true, :desc => "ns2 param" }, "method_param" => { :required => false, :desc => "method param" } } } ] end it "should not symbolize params" do diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 02619ccfc..d6ff1c5e4 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -268,7 +268,7 @@ def app; subject end redirect "/ha", :permanent => true end get '/hey' - last_response.status.should eq 301 + last_response.status.should eq 304 last_response.headers['Location'].should eq "/ha" last_response.body.should eq "" end @@ -347,20 +347,6 @@ def memoized last_response.body.should == 'Hiya' end - it 'should automatically use Klass::Entity if that exists' do - some_model = Class.new - entity = Class.new(Grape::Entity) - entity.stub!(:represent).and_return("Auto-detect!") - - some_model.const_set :Entity, entity - - subject.get '/example' do - present some_model.new - end - get '/example' - last_response.body.should == 'Auto-detect!' - end - it 'should add a root key to the output if one is given' do subject.get '/example' do present({:abc => 'def'}, :root => :root) diff --git a/spec/grape/entity_spec.rb b/spec/grape/entity_spec.rb index 0b15bb74f..a108f0c53 100644 --- a/spec/grape/entity_spec.rb +++ b/spec/grape/entity_spec.rb @@ -253,6 +253,8 @@ describe '#serializable_hash' do + + it 'should not throw an exception if a nil options object is passed' do expect{ fresh_class.new(model).serializable_hash(nil) }.not_to raise_error end @@ -294,14 +296,6 @@ res = fresh_class.new(model).serializable_hash res.should have_key :non_existant_attribute end - - it "should not expose attributes that are generated by a block but have not passed criteria" do - fresh_class.expose :non_existant_attribute, :proc => lambda {|model, options| - "I exist, but it is not yet my time to shine" - }, :if => lambda { |model, options| false } - res = fresh_class.new(model).serializable_hash - res.should_not have_key :non_existant_attribute - end context "#serializable_hash" do @@ -375,73 +369,24 @@ def timestamp(date) rep.last.serializable_hash[:name].should == 'Friend 2' end - context 'child representations' do - it 'should disable root key name for child representations' do - - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name, :email - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity - end - - rep = subject.send(:value_for, :friends) - rep.should be_kind_of(Array) - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:name].should == 'Friend 1' - rep.last.serializable_hash[:name].should == 'Friend 2' - end - - it 'should pass through custom options' do - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name - expose :email, :if => { :user_type => :admin } - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity + it 'should disable root key name for child representations' do + + module EntitySpec + class FriendEntity < Grape::Entity + root 'friends', 'friend' + expose :name, :email end - - rep = subject.send(:value_for, :friends) - rep.should be_kind_of(Array) - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should be_nil - rep.last.serializable_hash[:email].should be_nil - - rep = subject.send(:value_for, :friends, { :user_type => :admin }) - rep.should be_kind_of(Array) - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should == 'friend1@example.com' - rep.last.serializable_hash[:email].should == 'friend2@example.com' end - - it 'should ignore the :collection parameter in the source options' do - module EntitySpec - class FriendEntity < Grape::Entity - root 'friends', 'friend' - expose :name - expose :email, :if => { :collection => true } - end - end - - fresh_class.class_eval do - expose :friends, :using => EntitySpec::FriendEntity - end - - rep = subject.send(:value_for, :friends, { :collection => false }) - rep.should be_kind_of(Array) - rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty - rep.first.serializable_hash[:email].should == 'friend1@example.com' - rep.last.serializable_hash[:email].should == 'friend2@example.com' + + fresh_class.class_eval do + expose :friends, :using => EntitySpec::FriendEntity end - + + rep = subject.send(:value_for, :friends) + rep.should be_kind_of(Array) + rep.reject{|r| r.is_a?(EntitySpec::FriendEntity)}.should be_empty + rep.first.serializable_hash[:name].should == 'Friend 1' + rep.last.serializable_hash[:name].should == 'Friend 2' end it 'should call through to the proc if there is one' do @@ -527,53 +472,5 @@ class FriendEntity < Grape::Entity subject.send(:conditions_met?, exposure_options, :true => true).should be_false end end - - describe "::DSL" do - subject{ Class.new } - - it 'should create an Entity class when called' do - subject.should_not be_const_defined(:Entity) - subject.send(:include, Grape::Entity::DSL) - subject.should be_const_defined(:Entity) - end - - context 'pre-mixed' do - before{ subject.send(:include, Grape::Entity::DSL) } - - it 'should be able to define entity traits through DSL' do - subject.entity do - expose :name - end - - subject.entity_class.exposures.should_not be_empty - end - - it 'should be able to expose straight from the class' do - subject.entity :name, :email - subject.entity_class.exposures.size.should == 2 - end - - it 'should be able to mix field and advanced exposures' do - subject.entity :name, :email do - expose :third - end - subject.entity_class.exposures.size.should == 3 - end - - context 'instance' do - let(:instance){ subject.new } - - describe '#entity' do - it 'should be an instance of the entity class' do - instance.entity.should be_kind_of(subject.entity_class) - end - - it 'should have an object of itself' do - instance.entity.object.should == instance - end - end - end - end - end end end diff --git a/spec/grape/middleware/formatter_spec.rb b/spec/grape/middleware/formatter_spec.rb index 871572857..fa58a4dc0 100644 --- a/spec/grape/middleware/formatter_spec.rb +++ b/spec/grape/middleware/formatter_spec.rb @@ -80,13 +80,6 @@ def to_xml subject.env['api.format'].should == :json end - it 'should use the format parameter if one is provided' do - subject.call({'PATH_INFO' => '/somewhere','QUERY_STRING' => 'format=json'}) - subject.env['api.format'].should == :json - subject.call({'PATH_INFO' => '/somewhere','QUERY_STRING' => 'format=xml'}) - subject.env['api.format'].should == :xml - end - it 'should use the default format if none is provided' do subject.call({'PATH_INFO' => '/info'}) subject.env['api.format'].should == :txt diff --git a/spec/grape/validations/coerce_spec.rb b/spec/grape/validations/coerce_spec.rb index f218d0b23..f7ba16bf0 100644 --- a/spec/grape/validations/coerce_spec.rb +++ b/spec/grape/validations/coerce_spec.rb @@ -111,19 +111,6 @@ class User last_response.status.should == 201 last_response.body.should == File.basename(__FILE__).to_s end - - it 'Nests integers' do - subject.params do - group :integers do - requires :int, :coerce => Integer - end - end - subject.get '/int' do params[:integers][:int].class; end - - get '/int', { :integers => { :int => "45" } } - last_response.status.should == 200 - last_response.body.should == 'Fixnum' - end end end end diff --git a/spec/grape/validations/presence_spec.rb b/spec/grape/validations/presence_spec.rb index 6680a817e..a8b2ba74e 100644 --- a/spec/grape/validations/presence_spec.rb +++ b/spec/grape/validations/presence_spec.rb @@ -26,29 +26,6 @@ class API < Grape::API get do "Hello" end - - params do - group :user do - requires :first_name, :last_name - end - end - get '/nested' do - "Nested" - end - - params do - group :admin do - requires :admin_name - group :super do - group :user do - requires :first_name, :last_name - end - end - end - end - get '/nested_triple' do - "Nested triple" - end end end end @@ -90,49 +67,5 @@ def app last_response.status.should == 200 last_response.body.should == "Hello" end - - it 'validates nested parameters' do - get('/nested') - last_response.status.should == 400 - last_response.body.should == "missing parameter: first_name" - - get('/nested', :user => {:first_name => "Billy"}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: last_name" - - get('/nested', :user => {:first_name => "Billy", :last_name => "Bob"}) - last_response.status.should == 200 - last_response.body.should == "Nested" - end - - it 'validates triple nested parameters' do - get('/nested_triple') - last_response.status.should == 400 - last_response.body.should == "missing parameter: admin_name" - - get('/nested_triple', :user => {:first_name => "Billy"}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: admin_name" - - get('/nested_triple', :admin => {:super => {:first_name => "Billy"}}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: admin_name" - - get('/nested_triple', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: admin_name" - - get('/nested_triple', :admin => {:super => {:user => {:first_name => "Billy"}}}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: admin_name" - - get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy"}}}) - last_response.status.should == 400 - last_response.body.should == "missing parameter: last_name" - - get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}}}) - last_response.status.should == 200 - last_response.body.should == "Nested triple" - end - + end