Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtus::Lite #299

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,33 @@ Blog.attribute_set[:posts].member_type.primitive # => Post
Post.attribute_set[:blog].type.primitive # => Blog
```

Virtus::Lite
------------

Virtus::Lite is a drop-in replacement for Virtus.model that supports the core
feature set of Virtus (type coercion, custom coercion, and default values),
while delivering 10x the speed. Many apps will be able to simply replace
`include Virtus.model` with `include Virtus::Lite` to enjoy this performance
boost.

```
Comparison:
FastAttributes: without values : 2326399.8 i/s
Virtus::Lite: without values : 319304.0 i/s - 7.29x slower
FastAttributes: integer values for integer attributes: 82969.3 i/s - 28.04x slower
FastAttributes: string values for integer attributes : 64758.7 i/s - 35.92x slower
Virtus::Lite: string values for integer attributes : 37270.1 i/s - 62.42x slower
Virtus::Lite: integer values for integer attributes : 36577.8 i/s - 63.60x slower
Virtus: integer values for integer attributes : 21857.4 i/s - 106.44x slower
Attrio: integer values for integer attributes : 11362.2 i/s - 204.75x slower
Virtus: without values : 9551.8 i/s - 243.56x slower
Attrio: string values for integer attributes : 9048.5 i/s - 257.10x slower
Attrio: without values : 6876.1 i/s - 338.33x slower
Virtus: string values for integer attributes : 3197.6 i/s - 727.55x slower
```

Adapted from the [fast_attributes benchmark code.](https://github.com/applift/fast_attributes/blob/master/benchmarks/comparison.rb)

Ruby version support
--------------------

Expand Down
2 changes: 2 additions & 0 deletions lib/virtus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,5 @@ def self.warn(msg)
require 'virtus/attribute/collection'
require 'virtus/attribute/hash'
require 'virtus/attribute/embedded_value'

require 'virtus/lite'
123 changes: 123 additions & 0 deletions lib/virtus/lite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
module Virtus
module Lite

class Boolean; end

module ClassMethods

attr_reader :attributes
attr_reader :set_attributes

def attribute( name, type = nil, options = {} )
@attributes ||= []
@attributes << name unless @attributes.include? name
define_method( "#{ name }=") do |value|
@set_attributes ||= []
@set_attributes << name
instance_variable_set "@#{ name }", value
end
define_method( name ) do
value = instance_variable_get "@#{ name }"
coercer = options[ :coercer ]
default = options[ :default ]
if coercer
coercer.call value
elsif value.nil? and not default.nil? and default.is_a? Symbol and respond_to? default
send default
else
if type.is_a? Array
if value.nil?
[]
elsif value.is_a? Array
value.map { |v| determine_value name, v, type.first, options }
else
determine_value name, value, type.first, options
end
else
determine_value name, value, type, options
end
end
end
# private :"#{ name }" if options[ :reader ] == :private
# private :"#{ name }=" if options[ :writer ] == :private
self
end
end

def self.included( base )
base.extend ClassMethods
end

def attributes
Hash[ self.class.attributes.map { |v| [ v, send( v ) ] } ] || {}
end

def []( key )
send( key ) if self.class.attributes.include? key
end

private

def determine_value( name, value, type, options )
default = options[ :default ]
if value.nil? and not default.nil?
value = default
elsif type and not value.is_a? type
if not value.nil? and @set_attributes.include? name
if value.is_a? Hash and not type.is_a? Hash
value = type.new value
else
value = convert value, type
end
end
end
value
end

def initialize( params = {} )
unless params.nil?
params.each do |name, value|
self.send( :"#{name}=", value ) if respond_to? "#{ name }="
end
end
self.class.send( :define_method, :to_hash ) do
Hash[ self.class.attributes.map { |attribute| [ attribute, send( attribute ) ] } ]
end
end

def convert( value, type )
case type.to_s
when 'String'
value.to_s
when 'Integer', 'Fixnum', 'Bignum'
if value.respond_to? :to_i
value.to_i
elsif value.is_a? TrueClass or value.is_a? FalseClass
value ? 1 : 0
else
0
end
when 'Float'
if value.respond_to? :to_f
value.to_f
elsif value.is_a? TrueClass or value.is_a? FalseClass
value ? 1.0 : 0.0
end
when 'Virtus::Attribute::Boolean', 'Virtus::Lite::Boolean'
if value.nil?
false
elsif value.is_a? String
value != ''
elsif value.is_a? Integer
value != 0
elsif value.is_a? TrueClass or value.is_a? FalseClass
value
elsif value.is_a? Float
value != 0.0
else
true
end
end
end
end
end
33 changes: 33 additions & 0 deletions spec/unit/virtus/lite/array_integer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'

class PlainArrayIntegerTest
include Virtus::Lite
attribute :fish, Array[Integer]
end

describe PlainArrayIntegerTest do
before do
@test = PlainArrayIntegerTest.new
end

it 'returns array of integers when set to array of strings' do
@test.fish = [ 'red', 'blue' ]
expect( @test.fish ).to eq( [ 0, 0 ] )
end
it 'returns array of integers when set to array of integers' do
@test.fish = [ 1, 2 ]
expect( @test.fish ).to eq( [ 1, 2 ] )
end
it 'returns array of integers when set to array of booleans' do
@test.fish = [ true, false ]
expect( @test.fish ).to eq( [ 1, 0 ] )
end
it 'returns array of integers when set to array of floats' do
@test.fish = [ 7.6, 23.5 ]
expect( @test.fish ).to eq( [ 7, 23 ] )
end
it 'returns array of integers when set to a mixed array' do
@test.fish = [ 'red', 42, false, 34.5, Integer ]
expect( @test.fish ).to eq( [ 0, 42, 0, 34, 0 ] )
end
end
33 changes: 33 additions & 0 deletions spec/unit/virtus/lite/array_integer_spec.rb-e
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'

class PlainArrayIntegerTest
include Subvirtus
attribute :fish, Array[Integer]
end

describe PlainArrayIntegerTest do
before do
@test = PlainArrayIntegerTest.new
end

it 'returns array of integers when set to array of strings' do
@test.fish = [ 'red', 'blue' ]
expect( @test.fish ).to eq( [ 0, 0 ] )
end
it 'returns array of integers when set to array of integers' do
@test.fish = [ 1, 2 ]
expect( @test.fish ).to eq( [ 1, 2 ] )
end
it 'returns array of integers when set to array of booleans' do
@test.fish = [ true, false ]
expect( @test.fish ).to eq( [ 1, 0 ] )
end
it 'returns array of integers when set to array of floats' do
@test.fish = [ 7.6, 23.5 ]
expect( @test.fish ).to eq( [ 7, 23 ] )
end
it 'returns array of integers when set to a mixed array' do
@test.fish = [ 'red', 42, false, 34.5, Integer ]
expect( @test.fish ).to eq( [ 0, 42, 0, 34, 0 ] )
end
end
33 changes: 33 additions & 0 deletions spec/unit/virtus/lite/array_string_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'

class PlainArrayStringTest
include Virtus::Lite
attribute :fish, Array[String]
end

describe PlainArrayStringTest do
before do
@test = PlainArrayStringTest.new
end

it 'returns array of strings when set to array of strings' do
@test.fish = [ 'red', 'blue' ]
expect( @test.fish ).to eq( [ 'red', 'blue' ] )
end
it 'returns array of strings when set to array of integers' do
@test.fish = [ 1, 2 ]
expect( @test.fish ).to eq( [ '1', '2' ] )
end
it 'returns array of strings when set to array of booleans' do
@test.fish = [ true, false ]
expect( @test.fish ).to eq( [ 'true', 'false' ] )
end
it 'returns array of strings when set to array of floats' do
@test.fish = [ 7.6, 23.5 ]
expect( @test.fish ).to eq( [ '7.6', '23.5' ] )
end
it 'returns array of strings when set to a mixed array' do
@test.fish = [ 'red', 42, false, 34.5, String ]
expect( @test.fish ).to eq( [ 'red', '42', 'false', '34.5', 'String' ] )
end
end
33 changes: 33 additions & 0 deletions spec/unit/virtus/lite/array_string_spec.rb-e
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'spec_helper'

class PlainArrayStringTest
include Subvirtus
attribute :fish, Array[String]
end

describe PlainArrayStringTest do
before do
@test = PlainArrayStringTest.new
end

it 'returns array of strings when set to array of strings' do
@test.fish = [ 'red', 'blue' ]
expect( @test.fish ).to eq( [ 'red', 'blue' ] )
end
it 'returns array of strings when set to array of integers' do
@test.fish = [ 1, 2 ]
expect( @test.fish ).to eq( [ '1', '2' ] )
end
it 'returns array of strings when set to array of booleans' do
@test.fish = [ true, false ]
expect( @test.fish ).to eq( [ 'true', 'false' ] )
end
it 'returns array of strings when set to array of floats' do
@test.fish = [ 7.6, 23.5 ]
expect( @test.fish ).to eq( [ '7.6', '23.5' ] )
end
it 'returns array of strings when set to a mixed array' do
@test.fish = [ 'red', 42, false, 34.5, String ]
expect( @test.fish ).to eq( [ 'red', '42', 'false', '34.5', 'String' ] )
end
end
22 changes: 22 additions & 0 deletions spec/unit/virtus/lite/attribute_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'

describe 'include Virtus::Lite' do

let :model do
Class.new do
include Virtus::Lite
end
end

it 'responds to #attribute' do
expect( model ).to respond_to( :attribute )
end

it 'responds to #attributes' do
expect( model ).to respond_to( :attributes )
end

it 'responds to #set_attributes' do
expect( model ).to respond_to( :set_attributes )
end
end
22 changes: 22 additions & 0 deletions spec/unit/virtus/lite/attribute_spec.rb-e
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'spec_helper'

describe 'include Subvirtus' do

let :model do
Class.new do
include Subvirtus
end
end

it 'responds to #attribute' do
expect( model ).to respond_to( :attribute )
end

it 'responds to #attributes' do
expect( model ).to respond_to( :attributes )
end

it 'responds to #set_attributes' do
expect( model ).to respond_to( :set_attributes )
end
end
Loading