Skip to content

Commit

Permalink
Merge 1e9aaca into f8b52e3
Browse files Browse the repository at this point in the history
  • Loading branch information
dnesteryuk committed Oct 22, 2019
2 parents f8b52e3 + 1e9aaca commit 8eb5248
Show file tree
Hide file tree
Showing 27 changed files with 398 additions and 315 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#### Features

* Your contribution here.
* [#1920](https://github.com/ruby-grape/grape/pull/1920): Replace Virtus with dry-types - [@dnesteryuk](https://github.com/dnesteryuk).
* [#1915](https://github.com/ruby-grape/grape/pull/1915): Micro optimizations in allocating hashes and arrays - [@dnesteryuk](https://github.com/dnesteryuk).
* [#1904](https://github.com/ruby-grape/grape/pull/1904): Allows Grape to load files on startup rather than on the first call - [@myxoh](https://github.com/myxoh).
* [#1907](https://github.com/ruby-grape/grape/pull/1907): Adds outside configuration to Grape with `configure` - [@unleashy](https://github.com/unleashy).
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ group :test do
gem 'rack-test', '~> 1.1.0'
gem 'rspec', '~> 3.0'
gem 'ruby-grape-danger', '~> 0.1.0', require: false
gem 'virtus'
end
33 changes: 33 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
Upgrading Grape
===============

### Upgrading to >= 1.2.5

[Virtus](https://github.com/solnic/virtus) isn't used for coercing anymore. If your project depends on Virtus, you have to add it to your `Gemfile`. Also, if Virtus is used as a custom type

```ruby
class User
include Virtus.model

attribute :id, Integer
attribute :name, String
end

# somewhere in your API
params do
requires :user, type: User
end
```

you need to add a class-level `parse` method to the model:

```ruby
class User
include Virtus.model

attribute :id, Integer
attribute :name, String

def self.parse(attrs)
new(attrs)
end
end
```

### Upgrading to >= 1.2.4

#### Headers in `error!` call
Expand Down
2 changes: 1 addition & 1 deletion grape.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0'
s.add_runtime_dependency 'rack', '>= 1.3.0'
s.add_runtime_dependency 'rack-accept'
s.add_runtime_dependency 'virtus', '>= 1.0.0'
s.add_runtime_dependency 'dry-types', '~> 1.1.1'

s.files = Dir['**/*'].keep_if { |file| File.file?(file) }
s.test_files = Dir['spec/**/*']
Expand Down
2 changes: 0 additions & 2 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
require 'i18n'
require 'thread'

require 'virtus'

I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)

module Grape
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/dsl/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def include_all_in_scope

def define_boolean_in_mod(mod)
return if defined? mod::Boolean
mod.const_set('Boolean', Virtus::Attribute::Boolean)
mod.const_set('Boolean', Grape::API::Boolean)
end

def inject_api_helpers_to_mod(mod, &_block)
Expand Down
4 changes: 2 additions & 2 deletions lib/grape/validations/params_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ def validate_value_coercion(coerce_type, *values_list)
values_list.each do |values|
next if !values || values.is_a?(Proc)
value_types = values.is_a?(Range) ? [values.begin, values.end] : values
if coerce_type == Virtus::Attribute::Boolean
value_types = value_types.map { |type| Virtus::Attribute.build(type) }
if coerce_type == Grape::API::Boolean
value_types = value_types.map { |type| Grape::API::Boolean.build(type) }
end
unless value_types.all? { |v| v.is_a? coerce_type }
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
Expand Down
35 changes: 5 additions & 30 deletions lib/grape/validations/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
require_relative 'types/json'
require_relative 'types/file'

# Patch for Virtus::Attribute::Collection
# See the file for more details
require_relative 'types/virtus_collection_patch'

module Grape
module Validations
# Module for code related to grape's system for
Expand All @@ -27,8 +23,7 @@ module Types
# a parameter value could not be coerced.
class InvalidValue; end

# Types representing a single value, which are coerced through Virtus
# or special logic in Grape.
# Types representing a single value, which are coerced.
PRIMITIVES = [
# Numerical
Integer,
Expand All @@ -42,10 +37,12 @@ class InvalidValue; end
Time,

# Misc
Virtus::Attribute::Boolean,
Grape::API::Boolean,
String,
Symbol,
Rack::Multipart::UploadedFile
Rack::Multipart::UploadedFile,
TrueClass,
FalseClass
].freeze

# Types representing data structures.
Expand Down Expand Up @@ -86,8 +83,6 @@ def self.primitive?(type)
# @param type [Class] type to check
# @return [Boolean] whether or not the type is known by Grape as a valid
# data structure type
# @note This method does not yet consider 'complex types', which inherit
# Virtus.model.
def self.structure?(type)
STRUCTURES.include?(type)
end
Expand All @@ -104,25 +99,6 @@ def self.multiple?(type)
(type.is_a?(Array) || type.is_a?(Set)) && type.size > 1
end

# Does the given class implement a type system that Grape
# (i.e. the underlying virtus attribute system) supports
# out-of-the-box? Currently supported are +axiom-types+
# and +virtus+.
#
# The type will be passed to +Virtus::Attribute.build+,
# and the resulting attribute object will be expected to
# respond correctly to +coerce+ and +value_coerced?+.
#
# @param type [Class] type to check
# @return [Boolean] +true+ where the type is recognized
def self.recognized?(type)
return false if type.is_a?(Array) || type.is_a?(Set)

type.is_a?(Virtus::Attribute) ||
type.ancestors.include?(Axiom::Types::Type) ||
type.include?(Virtus::Model::Core)
end

# Does Grape provide special coercion and validation
# routines for the given class? This does not include
# automatic handling for primitives, structures and
Expand Down Expand Up @@ -152,7 +128,6 @@ def self.custom?(type)
!primitive?(type) &&
!structure?(type) &&
!multiple?(type) &&
!recognized?(type) &&
!special?(type) &&
type.respond_to?(:parse) &&
type.method(:parse).arity == 1
Expand Down
54 changes: 54 additions & 0 deletions lib/grape/validations/types/array_coercer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require_relative 'dry_type_coercer'

module Grape
module Validations
module Types
# Coerces elements in an array. It might be an array of strings or integers or
# anything else.
#
# It could've been possible to use an +of+
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
# provided by dry-types. Unfortunately, it doesn't work for Grape because of
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
# maintains Virtus behavior in coercing.
class ArrayCoercer < DryTypeCoercer
def initialize(type, strict = false)
super

@coercer = scope::Array
@elem_coercer = PrimitiveCoercer.new(type.first, strict)
end

def call(_val)
collection = super

return collection if collection.is_a?(InvalidValue)

coerce_elements collection
end

protected

def coerce_elements(collection)
collection.each_with_index do |elem, index|
return InvalidValue.new if would_virtus_reject?(elem)

coerced_elem = @elem_coercer.call(elem)

return coerced_elem if coerced_elem.is_a?(InvalidValue)

collection[index] = coerced_elem
end

collection
end

# This method maintaine logic which was defined by Virtus for arrays.
# Virtus doesn't allow nil in arrays.
def would_virtus_reject?(val)
val.nil?
end
end
end
end
end
91 changes: 43 additions & 48 deletions lib/grape/validations/types/build_coercer.rb
Original file line number Diff line number Diff line change
@@ -1,78 +1,73 @@
require_relative 'array_coercer'
require_relative 'set_coercer'
require_relative 'primitive_coercer'

module Grape
module Validations
module Types
# Work out the +Virtus::Attribute+ object to
# use for coercing strings to the given +type+.
# Coercion +method+ will be inferred if none is
# supplied.
# Chooses the best coercer for the given type. For example, if the type
# is Integer, it will return a coercer which will be able to coerce a value
# to the integer.
#
# There are a few very special coercers which might be returned.
#
# +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when
# the given type implies values in an array with different types.
# For example, +[Integer, String]+ allows integer and string values in
# an array.
#
# +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when
# a method is specified by a user with +coerce_with+ option or the user
# specifies a custom type which implements requirments of
# +Grape::Types::CustomTypeCoercer+.
#
# If a +Virtus::Attribute+ object already built
# with +Virtus::Attribute.build+ is supplied as
# the +type+ it will be returned and +method+
# will be ignored.
# +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the
# previous one, but it expects an array or set of values having a custom
# type implemented by the user.
#
# See {CustomTypeCoercer} for further details
# about coercion and type-checking inference.
# There is also a group of custom types implemented by Grape, check
# +Grape::Validations::Types::SPECIAL+ to get the full list.
#
# @param type [Class] the type to which input strings
# should be coerced
# @param method [Class,#call] the coercion method to use
# @return [Virtus::Attribute] object to be used
# @return [Object] object to be used
# for coercion and type validation
def self.build_coercer(type, method = nil)
cache_instance(type, method) do
create_coercer_instance(type, method)
def self.build_coercer(type, method: nil, strict: false)
cache_instance(type, method, strict) do
create_coercer_instance(type, method, strict)
end
end

def self.create_coercer_instance(type, method = nil)
# Accept pre-rolled virtus attributes without interference
return type if type.is_a? Virtus::Attribute

converter_options = {
nullify_blank: true
}
conversion_type = if method == JSON
Object
# because we want just parsed JSON content:
# if type is Array and data is `"{}"`
# result will be [] because Virtus converts hashes
# to arrays
else
type
end

def self.create_coercer_instance(type, method, strict)
# Use a special coercer for multiply-typed parameters.
if Types.multiple?(type)
converter_options[:coercer] = Types::MultipleTypeCoercer.new(type, method)
conversion_type = Object
MultipleTypeCoercer.new(type, method)

# Use a special coercer for custom types and coercion methods.
elsif method || Types.custom?(type)
converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method)
CustomTypeCoercer.new(type, method)

# Special coercer for collections of types that implement a parse method.
# CustomTypeCoercer (above) already handles such types when an explicit coercion
# method is supplied.
elsif Types.collection_of_custom?(type)
converter_options[:coercer] = Types::CustomTypeCollectionCoercer.new(
Types::CustomTypeCollectionCoercer.new(
type.first, type.is_a?(Set)
)

# Grape swaps in its own Virtus::Attribute implementations
# for certain special types that merit first-class support
# (but not if a custom coercion method has been supplied).
elsif Types.special?(type)
conversion_type = Types::SPECIAL[type]
Types::SPECIAL[type].new
elsif type.is_a?(Array)
ArrayCoercer.new type, strict
elsif type.is_a?(Set)
SetCoercer.new type, strict
else
PrimitiveCoercer.new type, strict
end

# Virtus will infer coercion and validation rules
# for many common ruby types.
Virtus::Attribute.build(conversion_type, converter_options)
end

def self.cache_instance(type, method, &_block)
key = cache_key(type, method)
def self.cache_instance(type, method, strict, &_block)
key = cache_key(type, method, strict)

return @__cache[key] if @__cache.key?(key)

Expand All @@ -85,8 +80,8 @@ def self.cache_instance(type, method, &_block)
instance
end

def self.cache_key(type, method)
[type, method].compact.map(&:to_s).join('_')
def self.cache_key(type, method, strict)
[type, method, strict].compact.map(&:to_s).join('_')
end

instance_variable_set(:@__cache, {})
Expand Down
Loading

0 comments on commit 8eb5248

Please sign in to comment.