virtus-2.0.0/0000755000004100000410000000000014072243150013073 5ustar www-datawww-datavirtus-2.0.0/.travis.yml0000644000004100000410000000066514072243150015213 0ustar www-datawww-datasudo: false language: ruby bundler_args: --without tools cache: bundler rvm: - 2.0 - 2.1 - 2.2 - 2.3 - 2.4 - 2.5 - 2.6 - 2.7 - 3.0 - jruby before_script: - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build after_script: - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT virtus-2.0.0/.rspec0000644000004100000410000000002714072243150014207 0ustar www-datawww-data--color --order random virtus-2.0.0/README.md0000644000004100000410000003351414072243150014360 0ustar www-datawww-data[gem]: https://rubygems.org/gems/virtus [travis]: https://travis-ci.org/solnic/virtus [codeclimate]: https://codeclimate.com/github/solnic/virtus [coveralls]: https://coveralls.io/r/solnic/virtus [inchpages]: http://inch-ci.org/github/solnic/virtus/ DISCONTINUED ------------ > Working on virtus taught me a lot about handling data in Ruby, which involves coercions, type safety and validation (amongst other things). Even though the project has been successful, and serving well for many people, I decided to build something better. As a result, [dry-types](https://github.com/dry-rb/dry-types), [dry-struct](https://github.com/dry-rb/dry-struct) and [dry-schema](https://github.com/dry-rb/dry-schema) were born. These projects should be considered as virtus' successors, with better separation of concerns and better features. If you're interested in a modern take on same problems that virtus tried to solve, please check out these projects! > > @solnic Virtus ====== [![Gem Version](https://badge.fury.io/rb/virtus.svg)][gem] [![Build Status](https://travis-ci.org/solnic/virtus.svg?branch=master)][travis] [![Code Climate](https://codeclimate.com/github/solnic/virtus/badges/gpa.svg)][codeclimate] [![Test Coverage](https://codeclimate.com/github/solnic/virtus/badges/coverage.svg)][codeclimate] [![Inline docs](http://inch-ci.org/github/solnic/virtus.svg?branch=master)][inchpages] Virtus allows you to define attributes on classes, modules or class instances with optional information about types, reader/writer method visibility and coercion behavior. It supports a lot of coercions and advanced mapping of embedded objects and collections. You can use it in many different contexts like: * Input parameter sanitization and coercion in web applications * Mapping JSON to domain objects * Encapsulating data-access in Value Objects * Domain model prototyping And probably more. Installation ------------ ``` terminal $ gem install virtus ``` or in your **Gemfile** ``` ruby gem 'virtus' ``` Examples -------- ### Using Virtus with Classes You can create classes extended with Virtus and define attributes: ``` ruby class User include Virtus.model attribute :name, String attribute :age, Integer attribute :birthday, DateTime end user = User.new(:name => 'Piotr', :age => 31) user.attributes # => { :name => "Piotr", :age => 31, :birthday => nil } user.name # => "Piotr" user.age = '31' # => 31 user.age.class # => Fixnum user.birthday = 'November 18th, 1983' # => # # mass-assignment user.attributes = { :name => 'Jane', :age => 21 } user.name # => "Jane" user.age # => 21 ``` ### Cherry-picking extensions ``` ruby # include attribute DSL + constructor + mass-assignment class User include Virtus.model attribute :name, String end user = User.new(:name => 'Piotr') user.attributes = { :name => 'John' } user.attributes # => {:name => 'John'} # include attribute DSL + constructor class User include Virtus.model(:mass_assignment => false) attribute :name, String end User.new(:name => 'Piotr') # include just the attribute DSL class User include Virtus.model(:constructor => false, :mass_assignment => false) attribute :name, String end user = User.new user.name = 'Piotr' ``` ### Using Virtus with Modules You can create modules extended with Virtus and define attributes for later inclusion in your classes: ```ruby module Name include Virtus.module attribute :name, String end module Age include Virtus.module(:coerce => false) attribute :age, Integer end class User include Name, Age end user = User.new(:name => 'John', :age => 30) ``` ### Dynamically Extending Instances It's also possible to dynamically extend an object with Virtus: ```ruby class User # nothing here end user = User.new user.extend(Virtus.model) user.attribute :name, String user.name = 'John' user.name # => 'John' ``` ### Default Values ``` ruby class Page include Virtus.model attribute :title, String # default from a singleton value (integer in this case) attribute :views, Integer, :default => 0 # default from a singleton value (boolean in this case) attribute :published, Boolean, :default => false # default from a callable object (proc in this case) attribute :slug, String, :default => lambda { |page, attribute| page.title.downcase.gsub(' ', '-') } # default from a method name as symbol attribute :editor_title, String, :default => :default_editor_title def default_editor_title published? ? title : "UNPUBLISHED: #{title}" end end page = Page.new(:title => 'Virtus README') page.slug # => 'virtus-readme' page.views # => 0 page.published # => false page.editor_title # => "UNPUBLISHED: Virtus README" page.views = 10 page.views # => 10 page.reset_attribute(:views) # => 0 page.views # => 0 ``` ### Default values on dynamically extended instances This requires you to set `:lazy` option because default values are set in the constructor if it's set to false (which is the default setting): ``` ruby User = Class.new user = User.new user.extend(Virtus.model) user.attribute :name, String, default: 'jane', lazy: true user.name # => "jane" ``` ### Embedded Value ``` ruby class City include Virtus.model attribute :name, String end class Address include Virtus.model attribute :street, String attribute :zipcode, String attribute :city, City end class User include Virtus.model attribute :name, String attribute :address, Address end user = User.new(:address => { :street => 'Street 1/2', :zipcode => '12345', :city => { :name => 'NYC' } }) user.address.street # => "Street 1/2" user.address.city.name # => "NYC" ``` ### Collection Member Coercions ``` ruby # Support "primitive" classes class Book include Virtus.model attribute :page_numbers, Array[Integer] end book = Book.new(:page_numbers => %w[1 2 3]) book.page_numbers # => [1, 2, 3] # Support EmbeddedValues, too! class Address include Virtus.model attribute :address, String attribute :locality, String attribute :region, String attribute :postal_code, String end class PhoneNumber include Virtus.model attribute :number, String end class User include Virtus.model attribute :phone_numbers, Array[PhoneNumber] attribute :addresses, Set[Address] end user = User.new( :phone_numbers => [ { :number => '212-555-1212' }, { :number => '919-444-3265' } ], :addresses => [ { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" } ]) user.phone_numbers # => [#, #] user.addresses # => #}> ``` ### Hash attributes coercion ``` ruby class Package include Virtus.model attribute :dimensions, Hash[Symbol => Float] end package = Package.new(:dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 }) package.dimensions # => { :width => 2.2, :height => 2.0, :length => 4.5 } ``` ### IMPORTANT note about Boolean type Be aware that some libraries may do a terrible thing and define a global Boolean constant which breaks virtus' constant type lookup, if you see issues with the boolean type you can workaround it like that: ``` ruby class User include Virtus.model attribute :admin, Axiom::Types::Boolean end ``` This will be improved in Virtus 2.0. ### IMPORTANT note about member coercions Virtus performs coercions only when a value is being assigned. If you mutate the value later on using its own interfaces then coercion won't be triggered. Here's an example: ``` ruby class Book include Virtus.model attribute :title, String end class Library include Virtus.model attribute :books, Array[Book] end library = Library.new # This will coerce Hash to a Book instance library.books = [ { :title => 'Introduction to Virtus' } ] # This WILL NOT COERCE the value because you mutate the books array with Array#<< library.books << { :title => 'Another Introduction to Virtus' } ``` A suggested solution to this problem would be to introduce your own class instead of using Array and implement mutation methods that perform coercions. For example: ``` ruby class Book include Virtus.model attribute :title, String end class BookCollection < Array def <<(book) if book.kind_of?(Hash) super(Book.new(book)) else super end end end class Library include Virtus.model attribute :books, BookCollection[Book] end library = Library.new library.books << { :title => 'Another Introduction to Virtus' } ``` ### Value Objects ``` ruby class GeoLocation include Virtus.value_object values do attribute :latitude, Float attribute :longitude, Float end end class Venue include Virtus.value_object values do attribute :name, String attribute :location, GeoLocation end end venue = Venue.new( :name => 'Pub', :location => { :latitude => 37.160317, :longitude => -98.437500 }) venue.location.latitude # => 37.160317 venue.location.longitude # => -98.4375 # Supports object's equality venue_other = Venue.new( :name => 'Other Pub', :location => { :latitude => 37.160317, :longitude => -98.437500 }) venue.location === venue_other.location # => true ``` ### Custom Coercions ``` ruby require 'json' class Json < Virtus::Attribute def coerce(value) value.is_a?(::Hash) ? value : JSON.parse(value) end end class User include Virtus.model attribute :info, Json, default: {} end user = User.new user.info = '{"email":"john@domain.com"}' # => {"email"=>"john@domain.com"} user.info.class # => Hash # With a custom attribute encapsulating coercion-specific configuration class NoisyString < Virtus::Attribute def coerce(value) value.to_s.upcase end end class User include Virtus.model attribute :scream, NoisyString end user = User.new(:scream => 'hello world!') user.scream # => "HELLO WORLD!" ``` ### Private Attributes ``` ruby class User include Virtus.model attribute :unique_id, String, :writer => :private def set_unique_id(id) self.unique_id = id end end user = User.new(:unique_id => '1234-1234') user.unique_id # => nil user.unique_id = '1234-1234' # => NoMethodError: private method `unique_id=' user.set_unique_id('1234-1234') user.unique_id # => '1234-1234' ``` ### Overriding setters ``` ruby class User include Virtus.model attribute :name, String def name=(new_name) custom_name = nil if new_name == "Godzilla" custom_name = "Can't tell" end super custom_name || new_name end end user = User.new(name: "Frank") user.name # => 'Frank' user = User.new(name: "Godzilla") user.name # => 'Can't tell' ``` ## Strict Coercion Mode By default Virtus returns the input value even when it couldn't coerce it to the expected type. If you want to catch such cases in a noisy way you can use the strict mode in which Virtus raises an exception when it failed to coerce an input value. ``` ruby class User include Virtus.model(:strict => true) attribute :admin, Boolean end # this will raise an error User.new :admin => "can't really say if true or false" ``` ## Nullify Blank Strings Mode If you want to replace empty Strings with `nil` values (since they can't be coerced into the expected type), you can use the `:nullify_blank` option. ``` ruby class User include Virtus.model(:nullify_blank => true) attribute :birthday, Date end User.new(:birthday => "").birthday # => nil ``` ## Building modules with custom configuration You can also build Virtus modules that contain their own configuration. ```ruby YupNopeBooleans = Virtus.model { |mod| mod.coerce = true mod.coercer.config.string.boolean_map = { 'nope' => false, 'yup' => true } } class User include YupNopeBooleans attribute :name, String attribute :admin, Boolean end # Or just include the module straight away ... class User include Virtus.model(:coerce => false) attribute :name, String attribute :admin, Boolean end ``` ## Attribute Finalization and Circular Dependencies If a type references another type which happens to not be available yet you need to use lazy-finalization of attributes and finalize virtus manually after all types have been already loaded: ``` ruby # in blog.rb class Blog include Virtus.model(:finalize => false) attribute :posts, Array['Post'] end # in post.rb class Post include Virtus.model(:finalize => false) attribute :blog, 'Blog' end # after loading both files just do: Virtus.finalize # constants will be resolved: Blog.attribute_set[:posts].member_type.primitive # => Post Post.attribute_set[:blog].type.primitive # => Blog ``` ## Plugins / Extensions List of plugins/extensions that add features to Virtus: * [virtus-localized](https://github.com/XescuGC/virtus-localized): Localize the attributes * [virtus-relations](https://github.com/smanolloff/virtus-relations): Add relations to Virtus objects Ruby version support -------------------- Virtus is known to work correctly with the following rubies: * 1.9.3 * 2.0.0 * 2.1.2 * jruby * (probably) rbx Credits ------- * Dan Kubb ([dkubb](https://github.com/dkubb)) * Chris Corbyn ([d11wtq](https://github.com/d11wtq)) * Emmanuel Gomez ([emmanuel](https://github.com/emmanuel)) * Fabio Rehm ([fgrehm](https://github.com/fgrehm)) * Ryan Closner ([rclosner](https://github.com/rclosner)) * Markus Schirp ([mbj](https://github.com/mbj)) * Yves Senn ([senny](https://github.com/senny)) Contributing ------------- * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Send me a pull request. Bonus points for topic branches. virtus-2.0.0/spec/0000755000004100000410000000000014072243150014025 5ustar www-datawww-datavirtus-2.0.0/spec/shared/0000755000004100000410000000000014072243150015273 5ustar www-datawww-datavirtus-2.0.0/spec/shared/constants_helpers.rb0000644000004100000410000000026614072243150021362 0ustar www-datawww-datamodule ConstantsHelpers extend self # helper to remove constants after test-runs. def undef_constant(mod, constant_name) mod.send(:remove_const, constant_name) end end virtus-2.0.0/spec/shared/idempotent_method_behaviour.rb0000644000004100000410000000016214072243150023373 0ustar www-datawww-datashared_examples_for 'an idempotent method' do it 'is idempotent' do is_expected.to equal(subject) end end virtus-2.0.0/spec/shared/options_class_method.rb0000644000004100000410000000073414072243150022044 0ustar www-datawww-datashared_examples_for 'an options class method' do context 'with no argument' do subject { object.send(method) } it { is_expected.to be(default) } end context 'with a default value' do subject { object.send(method, value) } let(:value) { mock('value') } it { is_expected.to equal(object) } it 'sets the default value for the class method' do expect { subject }.to change { object.send(method) }.from(default).to(value) end end end virtus-2.0.0/spec/shared/freeze_method_behavior.rb0000644000004100000410000000172214072243150022321 0ustar www-datawww-datashared_examples_for 'a #freeze method' do let(:sample_exception) do begin object.dup.freeze.instance_variable_set(:@foo, :bar) rescue => exception exception end end let(:expected_exception_class) do # Ruby 1.8 blows up with TypeError Ruby 1.9 with RuntimeError sample_exception.class end let(:expected_exception_message) do # Ruby 1.8 blows up with a different message than Ruby 1.9 sample_exception.message end it_should_behave_like 'an idempotent method' it 'returns object' do is_expected.to be(object) end it 'prevents future modifications' do subject expectation = raise_error(expected_exception_class,expected_exception_message) expect { object.instance_variable_set(:@foo, :bar) }.to(expectation) end describe '#frozen?' do subject { super().frozen? } it { is_expected.to be(true) } end it 'allows to access attribute' do expect(subject.name).to eql('John') end end virtus-2.0.0/spec/unit/0000755000004100000410000000000014072243150015004 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/0000755000004100000410000000000014072243150016340 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attributes_reader_spec.rb0000644000004100000410000000154314072243150023412 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#attributes' do shared_examples_for 'attribute hash' do it 'includes all attributes' do subject.attributes = { :test => 'Hello World', :test_priv => 'Yo' } expect(subject.attributes).to eql(:test => 'Hello World') end end context 'with a class' do let(:model) { Class.new { include Virtus attribute :test, String attribute :test_priv, String, :reader => :private } } it_behaves_like 'attribute hash' do subject { model.new } end end context 'with an instance' do subject { model.new } let(:model) { Class.new } before do subject.extend(Virtus) subject.attribute :test, String subject.attribute :test_priv, String, :reader => :private end it_behaves_like 'attribute hash' end end virtus-2.0.0/spec/unit/virtus/model_spec.rb0000644000004100000410000001127114072243150021001 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '.model' do shared_examples_for 'a model with constructor' do it 'accepts attribute hash' do instance = subject.new(:name => 'Jane') expect(instance.name).to eql('Jane') end end shared_examples_for 'a model with mass-assignment' do let(:attributes) do { :name => 'Jane', :something => nil } end before do instance.attributes = attributes end it 'accepts attribute hash' do expect(instance.attributes).to eql(attributes) end end shared_examples_for 'a model with strict mode turned off' do it 'has attributes with strict set to false' do expect(subject.send(:attribute_set)[:name]).to_not be_strict end end context 'with default configuration' do let(:mod) { Virtus.model } context 'with a class' do let(:model) { Class.new } subject { model } before do subject.send(:include, mod) subject.attribute :name, String, :default => 'Jane' subject.attribute :something end it_behaves_like 'a model with constructor' it_behaves_like 'a model with strict mode turned off' it_behaves_like 'a model with mass-assignment' do let(:instance) { subject.new } end it 'defaults to Object for attribute type' do expect(model.attribute_set[:something].type).to be(Axiom::Types::Object) end context 'with a sub-class' do subject { Class.new(model) } before do subject.attribute :age, Integer end it_behaves_like 'a model with constructor' it_behaves_like 'a model with strict mode turned off' it_behaves_like 'a model with mass-assignment' do let(:instance) { subject.new } let(:attributes) { { :name => 'Jane', :something => nil, :age => 23 } } end it 'has its own attributes' do expect(subject.attribute_set[:age]).to be_kind_of(Virtus::Attribute) end end end context 'with an instance' do subject { Class.new.new } before do subject.extend(mod) subject.attribute :name, String subject.attribute :something end it_behaves_like 'a model with strict mode turned off' it_behaves_like 'a model with mass-assignment' do let(:instance) { subject } end end end context 'when constructor is disabled' do subject { Class.new.send(:include, mod) } let(:mod) { Virtus.model(:constructor => false) } it 'does not accept attribute hash in the constructor' do expect { subject.new({}) }.to raise_error(ArgumentError) end end context 'when strict mode is enabled' do let(:mod) { Virtus.model(:strict => true) } let(:model) { Class.new } context 'with a class' do subject { model.new } before do model.send(:include, mod) model.attribute :name, String end it 'has attributes with strict set to true' do expect(model.attribute_set[:name]).to be_strict end end context 'with an instance' do subject { model.new } before do subject.extend(mod) subject.attribute :name, String end it 'has attributes with strict set to true' do expect(subject.send(:attribute_set)[:name]).to be_strict end end end context 'when mass-assignment is disabled' do let(:mod) { Virtus.model(:mass_assignment => false) } let(:model) { Class.new } context 'with a class' do subject { model.new } before do model.send(:include, mod) end it { is_expected.not_to respond_to(:attributes) } it { is_expected.not_to respond_to(:attributes=) } end context 'with an instance' do subject { model.new } before do subject.extend(mod) end it { is_expected.not_to respond_to(:attributes) } it { is_expected.not_to respond_to(:attributes=) } end end context 'when :required is set' do let(:mod) { Virtus.model(:required => false) } let(:model) { Class.new } context 'with a class' do subject { model.new } before do model.send(:include, mod) model.attribute :name, String end it 'has attributes with :required option inherited from module' do expect(model.attribute_set[:name]).to_not be_required end end context 'with an instance' do subject { model.new } before do subject.extend(mod) subject.attribute :name, String end it 'has attributes with strict set to true' do expect(subject.send(:attribute_set)[:name]).not_to be_required end end end end virtus-2.0.0/spec/unit/virtus/set_default_attributes_spec.rb0000644000004100000410000000113014072243150024437 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#set_default_attributes!' do subject { object.set_default_attributes! } let(:model) { Class.new { include Virtus attribute :name, String, :default => 'foo', :lazy => true attribute :age, Integer, :default => 30 } } let(:object) { model.new } before do object.set_default_attributes! end it { is_expected.to be(object) } describe '#name' do subject { super().name } it { is_expected.to eql('foo') } end describe '#age' do subject { super().age } it { is_expected.to be(30) } end end virtus-2.0.0/spec/unit/virtus/element_reader_spec.rb0000644000004100000410000000043414072243150022653 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#[]' do subject { object[:test] } let(:model) { Class.new { include Virtus attribute :test, String } } let(:object) { model.new } before do object.test = 'foo' end it { is_expected.to eq('foo') } end virtus-2.0.0/spec/unit/virtus/value_object_spec.rb0000644000004100000410000000656214072243150022352 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::ValueObject do shared_examples_for 'a valid value object' do subject { model.new(attributes) } let(:attributes) { Hash[:id => 1, :name => 'Jane Doe'] } describe '#id' do subject { super().id } it { is_expected.to be(1) } end describe '#name' do subject { super().name } it { is_expected.to eql('Jane Doe') } end it 'sets private writers' do expect(subject.class.attribute_set[:id]).to_not be_public_writer expect(subject.class.attribute_set[:name]).to_not be_public_writer end it 'disallows cloning' do expect(subject.clone).to be(subject) end it 'defines #eql?' do expect(subject).to eql(subject.class.new(attributes)) end it 'defines #==' do expect(subject == subject.class.new(attributes)).to be(true) end it 'defines #hash' do expect(subject.hash).to eql(subject.class.new(attributes).hash) end it 'defines #inspect' do expect(subject.inspect).to eql( %(#) ) end it 'allows to construct new values using #with' do new_instance = subject.with(:name => "John Doe") expect(new_instance.id).to eql(subject.id) expect(new_instance.name).to eql("John Doe") end end shared_examples_for 'a valid value object with mass-assignment turned on' do subject { model.new } it 'disallows mass-assignment' do expect(subject.private_methods).to include(:attributes=) end end context 'using new values {} block' do let(:model) { model = Virtus.value_object(:coerce => false, :mass_assignment => mass_assignment) Class.new { include model def self.name 'Model' end values do attribute :id, Integer attribute :name, String end } } context 'without mass-assignment' do let(:mass_assignment) { false } it_behaves_like 'a valid value object' end context 'with mass-assignment' do let(:mass_assignment) { true } it_behaves_like 'a valid value object' it_behaves_like 'a valid value object with mass-assignment turned on' context 'with a model subclass' do let(:subclass) { Class.new(model) { values do attribute :email, String end } } it_behaves_like 'a valid value object' do subject { subclass.new(attributes) } let(:attributes) { Hash[:id => 1, :name => 'Jane Doe', :email => 'jane@doe.com'] } describe '#email' do subject { super().email } it { is_expected.to eql('jane@doe.com') } end it 'sets private writers for additional values' do expect(subclass.attribute_set[:email]).to_not be_public_writer end it 'defines valid #== for a subclass' do expect(subject == subject.class.new(attributes.merge(:id => 2))).to be(false) end end end end end context 'using deprecated inclusion' do let(:model) { Class.new { include Virtus::ValueObject def self.name 'Model' end attribute :id, Integer attribute :name, String } } it_behaves_like 'a valid value object' end end virtus-2.0.0/spec/unit/virtus/attribute/0000755000004100000410000000000014072243150020343 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/collection/0000755000004100000410000000000014072243150022476 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/collection/coerce_spec.rb0000644000004100000410000000434114072243150025277 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Collection, '#coerce' do subject { object.coerce(input) } context 'when input is an array' do context 'when member type is a primitive' do fake(:coercer) { Virtus::Attribute::Coercer } fake(:member_type) { Virtus::Attribute } let(:member_primitive) { Integer } let(:input) { ['1', '2'] } let(:object) { described_class.build(Array[member_primitive], :coercer => coercer, :member_type => member_type) } it 'uses coercer to coerce members' do mock(coercer).call(input) { input } mock(member_type).finalize { member_type } mock(member_type).coerce('1') { 1 } mock(member_type).coerce('2') { 2 } expect(subject).to eq([1, 2]) expect(member_type).to have_received.coerce('1') expect(member_type).to have_received.coerce('2') end end context 'when member type is an EV' do let(:member_primitive) { Struct.new(:id) } let(:input) { [1, 2] } let(:object) { described_class.build(Array[member_primitive]) } it 'coerces members' do expect(subject).to eq([member_primitive.new(1), member_primitive.new(2)]) end end context 'when member type is a hash with key/value coercion' do let(:member_primitive) { Hash[String => Integer] } let(:member_attribute) { Virtus::Attribute.build(member_primitive) } let(:input) { [{:one => '1'}, {:two => '2'}] } let(:output) { [member_attribute.coerce(input.first), member_attribute.coerce(input.last)] } let(:object) { described_class.build(Array[member_primitive]) } it 'coerces members' do expect(subject).to eq(output) end end end context 'when input is nil' do let(:input) { nil } fake(:coercer) { Virtus::Attribute::Coercer } fake(:member_type) { Virtus::Attribute } let(:member_primitive) { Integer } let(:object) { described_class.build( Array[member_primitive], coercer: coercer, member_type: member_type ) } it 'returns nil' do mock(coercer).call(input) { input } expect(subject).to be(input) end end end virtus-2.0.0/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb0000644000004100000410000000127014072243150030655 0ustar www-datawww-datarequire 'spec_helper' require 'set' describe Virtus::Attribute::Collection, '#value_coerced?' do subject { object.value_coerced?(input) } let(:object) { described_class.build(Array[Integer]) } context 'when input has correctly typed members' do let(:input) { [1, 2, 3] } it { is_expected.to be(true) } end context 'when input has incorrectly typed members' do let(:input) { [1, 2, '3'] } it { is_expected.to be(false) } end context 'when the collection type is incorrect' do let(:input) { Set[1, 2, 3] } it { is_expected.to be(false) } end context 'when the input is empty' do let(:input) { [] } it { is_expected.to be(true) } end end virtus-2.0.0/spec/unit/virtus/attribute/collection/class_methods/0000755000004100000410000000000014072243150025326 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb0000644000004100000410000000512014072243150027762 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '.build' do subject { described_class.build(type, options) } let(:options) { {} } shared_examples_for 'a valid collection attribute instance' do it { is_expected.to be_instance_of(Virtus::Attribute::Collection) } it { is_expected.to be_frozen } end context 'when type is Array' do let(:type) { Array } it_behaves_like 'a valid collection attribute instance' it 'sets default member type' do expect(subject.type.member_type).to be(Axiom::Types::Object) end end context 'when type is Array[Virtus::Attribute::Boolean]' do let(:type) { Array[Virtus::Attribute::Boolean] } it_behaves_like 'a valid collection attribute instance' it 'sets member type' do expect(subject.type.member_type).to be(Axiom::Types::Boolean) end end context 'when type is Array[Float]' do let(:type) { Array[Float] } it_behaves_like 'a valid collection attribute instance' it 'sets member type' do expect(subject.type.member_type).to be(Axiom::Types::Float) end end context 'when type is Array[String, Integer]' do let(:type) { Array[String, Integer] } specify do expect { subject }.to raise_error( NotImplementedError, "build SumType from list of types (#{type.inspect})" ) end end context 'when type is Set' do let(:type) { Set } it_behaves_like 'a valid collection attribute instance' it 'sets default member type' do expect(subject.type.member_type).to be(Axiom::Types::Object) end end context 'when type is Set[Float]' do let(:type) { Set[Float] } it_behaves_like 'a valid collection attribute instance' it 'sets member type' do expect(subject.type.member_type).to be(Axiom::Types::Float) end end context 'when type is an Enumerable' do let(:type) { Class.new { include Enumerable } } it_behaves_like 'a valid collection attribute instance' end context 'when type is Array subclass' do let(:type) { Class.new(Array) } it_behaves_like 'a valid collection attribute instance' end context 'when type is a custom collection instance' do let(:type) { Class.new(Array)[String] } it_behaves_like 'a valid collection attribute instance' it 'sets member type' do expect(subject.type.member_type).to be(Axiom::Types::String) end end context 'when strict mode is used' do let(:type) { Array[String] } let(:options) { { strict: true } } it 'sets strict mode for member type' do expect(subject.member_type).to be_strict end end end virtus-2.0.0/spec/unit/virtus/attribute/embedded_value/0000755000004100000410000000000014072243150023270 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb0000644000004100000410000000401114072243150026063 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::EmbeddedValue, '#coerce' do subject { object.coerce(input) } let(:object) { described_class.build(model, options) } let(:options) { {} } context 'when primitive is OpenStruct' do let(:model) { OpenStruct } context 'when input is an attribute hash' do let(:input) { Hash[name: 'Piotr', age: 30] } it { is_expected.to be_instance_of(model) } describe '#name' do subject { super().name } it { is_expected.to eql('Piotr') } end describe '#age' do subject { super().age } it { is_expected.to eql(30) } end end context 'when input is nil' do let(:input) { nil } it { is_expected.to be(nil) } end context 'when input is a model instance' do let(:input) { OpenStruct.new } it { is_expected.to be(input) } end end context 'when primitive is Struct' do let(:model) { Struct.new(:name, :age) } context 'when input is an attribute hash' do let(:input) { ['Piotr', 30] } it { is_expected.to be_instance_of(model) } describe '#name' do subject { super().name } it { is_expected.to eql('Piotr') } end describe '#age' do subject { super().age } it { is_expected.to eql(30) } end end context 'when input is nil' do let(:input) { nil } it { is_expected.to be(nil) } end context 'when input is a model instance' do let(:input) { model.new('Piotr', 30) } it { is_expected.to be(input) } end end context 'when :strict mode is enabled' do let(:model) { Struct.new(:name) } let(:options) { { :strict => true } } context 'when input is coercible' do let(:input) { ['Piotr'] } it { is_expected.to eql(model.new('Piotr')) } end context 'when input is not coercible' do let(:input) { nil } it 'raises error' do expect { subject }.to raise_error(Virtus::CoercionError) end end end end virtus-2.0.0/spec/unit/virtus/attribute/embedded_value/class_methods/0000755000004100000410000000000014072243150026120 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb0000644000004100000410000000351514072243150030562 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::EmbeddedValue, '.build' do subject { described_class.build(type) } context 'when type is a Virtus.model' do let(:type) { Class.new { include Virtus.model } } it { is_expected.to be_frozen } it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } describe '#coercer' do subject { super().coercer } it { is_expected.to be_instance_of(described_class::FromOpenStruct) } end end context 'when type includes Virtus' do let(:type) { Class.new { include Virtus } } it { is_expected.to be_frozen } it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } describe '#coercer' do subject { super().coercer } it { is_expected.to be_instance_of(described_class::FromOpenStruct) } end end context 'when type is an OpenStruct subclass' do let(:type) { Class.new(OpenStruct) } it { is_expected.to be_frozen } it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } describe '#coercer' do subject { super().coercer } it { is_expected.to be_instance_of(described_class::FromOpenStruct) } end end context 'when type is OpenStruct' do let(:type) { OpenStruct } it { is_expected.to be_frozen } it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } describe '#coercer' do subject { super().coercer } it { is_expected.to be_instance_of(described_class::FromOpenStruct) } end end context 'when type is Struct' do let(:type) { Struct.new(:test) } it { is_expected.to be_frozen } it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } describe '#coercer' do subject { super().coercer } it { is_expected.to be_instance_of(described_class::FromStruct) } end end end virtus-2.0.0/spec/unit/virtus/attribute/coerce_spec.rb0000644000004100000410000000706014072243150023145 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#coerce' do subject { object.coerce(input) } fake(:coercer) { Virtus::Attribute::Coercer } let(:object) { described_class.build(String, :coercer => coercer, :strict => strict, :required => required, :nullify_blank => nullify_blank) } let(:required) { true } let(:nullify_blank) { false } let(:input) { 1 } let(:output) { '1' } context 'when strict mode is turned off' do let(:strict) { false } it 'uses coercer to coerce the input value' do mock(coercer).call(input) { output } expect(subject).to be(output) expect(coercer).to have_received.call(input) end end context 'when strict mode is turned on' do let(:strict) { true } it 'uses coercer to coerce the input value' do mock(coercer).call(input) { output } mock(coercer).success?(String, output) { true } expect(subject).to be(output) expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, output) end context 'when attribute is not required and input is nil' do let(:required) { false } let(:input) { nil } it 'returns nil' do mock(coercer).call(input) { input } mock(coercer).success?(String, input) { false } expect(subject).to be(nil) expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, input) end end context 'when attribute is required and input is nil' do let(:input) { nil } it 'returns raises error' do mock(coercer).call(input) { input } mock(coercer).success?(String, input) { false } expect { subject }.to raise_error(Virtus::CoercionError) expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, input) end end it 'raises error when input was not coerced' do mock(coercer).call(input) { input } mock(coercer).success?(String, input) { false } expect { subject }.to raise_error(Virtus::CoercionError) expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, input) end end context 'when the input is an empty String' do let(:input) { '' } let(:output) { '' } context 'when nullify_blank is turned on' do let(:nullify_blank) { true } let(:strict) { false } let(:require) { false } it 'returns nil' do mock(coercer).call(input) { input } mock(coercer).success?(String, input) { false } expect(subject).to be_nil expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, input) end it 'returns the ouput if it was coerced' do mock(coercer).call(input) { output } mock(coercer).success?(String, output) { true } expect(subject).to be(output) expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, output) end end context 'when both nullify_blank and strict are turned on' do let(:nullify_blank) { true } let(:strict) { true } it 'does not raises an coercion error' do mock(coercer).call(input) { input } mock(coercer).success?(String, input) { false } expect { subject }.not_to raise_error expect(subject).to be_nil expect(coercer).to have_received.call(input) expect(coercer).to have_received.success?(String, input) end end end end virtus-2.0.0/spec/unit/virtus/attribute/set_spec.rb0000644000004100000410000000124314072243150022475 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#set' do subject { object.set(instance, value) } let(:object) { described_class.build(String, options.merge(:name => name)) } let(:model) { Class.new { attr_reader :test } } let(:name) { :test } let(:instance) { model.new } let(:value) { 'Jane Doe' } let(:options) { {} } it { is_expected.to be(value) } context 'without coercion' do specify do expect { subject }.to change { instance.test }.to(value) end end context 'with coercion' do let(:value) { :'Jane Doe' } specify do expect { subject }.to change { instance.test }.to('Jane Doe') end end end virtus-2.0.0/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb0000644000004100000410000000057614072243150026532 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#value_coerced?' do subject { object.value_coerced?(input) } let(:object) { described_class.build(String) } context 'when input is coerced' do let(:input) { '1' } it { is_expected.to be(true) } end context 'when input is not coerced' do let(:input) { 1 } it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/required_predicate_spec.rb0000644000004100000410000000063014072243150025541 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#required?' do subject { object.required? } let(:object) { described_class.build(String, :required => required) } context 'when required option is true' do let(:required) { true } it { is_expected.to be(true) } end context 'when required option is false' do let(:required) { false } it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/custom_collection_spec.rb0000644000004100000410000000141114072243150025424 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Collection, 'custom subclass' do subject { attribute_class.build(primitive) } let(:primitive) { Class.new { include Enumerable } } after do described_class.descendants.delete(attribute_class) end context 'when primitive is set on the attribute subclass' do let(:attribute_class) { Class.new(described_class).primitive(primitive) } describe '#primitive' do subject { super().primitive } it { is_expected.to be(primitive) } end end context 'when primitive is not set on the attribute subclass' do let(:attribute_class) { Class.new(described_class) } describe '#primitive' do subject { super().primitive } it { is_expected.to be(primitive) } end end end virtus-2.0.0/spec/unit/virtus/attribute/boolean/0000755000004100000410000000000014072243150021762 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/boolean/coerce_spec.rb0000644000004100000410000000167014072243150024565 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Boolean, '#coerce' do subject { object.coerce(input) } let(:object) { described_class.build('Boolean', options) } let(:options) { {} } context 'when strict is turned off' do context 'with a truthy value' do let(:input) { 1 } it { is_expected.to be(true) } end context 'with a falsy value' do let(:input) { 0 } it { is_expected.to be(false) } end end context 'when strict is turned on' do let(:options) { { :strict => true } } context 'with a coercible input' do let(:input) { 1 } it { is_expected.to be(true) } end context 'with a non-coercible input' do let(:input) { 'no idea if true or false' } it 'raises coercion error' do expect { subject }.to raise_error( Virtus::CoercionError, /Failed to coerce "no idea if true or false"/ ) end end end end virtus-2.0.0/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb0000644000004100000410000000075614072243150030151 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Boolean, '#value_coerced?' do subject { object.value_coerced?(input) } let(:object) { described_class.build('Boolean') } context 'when input is true' do let(:input) { true } it { is_expected.to be(true) } end context 'when input is false' do let(:input) { false } it { is_expected.to be(true) } end context 'when input is not coerced' do let(:input) { 1 } it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/hash/0000755000004100000410000000000014072243150021266 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/hash/coerce_spec.rb0000644000004100000410000000556514072243150024100 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Hash, '#coerce' do subject { object.coerce(input) } fake(:coercer) { Virtus::Attribute::Coercer } fake(:key_type) { Virtus::Attribute } fake(:value_type) { Virtus::Attribute } let(:object) { described_class.build(Hash[key_primitive => value_primitive], options) } let(:options) { {} } context 'when input is coercible to hash' do let(:input) { Class.new { def to_hash; { :hello => 'World' }; end }.new } let(:object) { described_class.build(Hash) } it { is_expected.to eq(:hello => 'World') } end context 'when input is not coercible to hash' do let(:input) { 'not really a hash' } let(:object) { described_class.build(Hash) } it { is_expected.to be(input) } end context 'when input is a hash' do context 'when key/value types are primitives' do let(:options) { { :coercer => coercer, :key_type => key_type, :value_type => value_type } } let(:key_primitive) { String } let(:value_primitive) { Integer } let(:input) { Hash[1 => '1', 2 => '2'] } it 'uses coercer to coerce key and value' do mock(coercer).call(input) { input } mock(key_type).finalize { key_type } mock(key_type).coerce(1) { '1' } mock(key_type).coerce(2) { '2' } mock(value_type).finalize { value_type } mock(value_type).coerce('1') { 1 } mock(value_type).coerce('2') { 2 } expect(subject).to eq(Hash['1' => 1, '2' => 2]) expect(key_type).to have_received.coerce(1) expect(key_type).to have_received.coerce(2) expect(value_type).to have_received.coerce('1') expect(value_type).to have_received.coerce('2') end end context 'when key/value types are EVs' do let(:key_primitive) { OpenStruct } let(:value_primitive) { Struct.new(:id) } let(:input) { Hash[{:name => 'Test'} => [1]] } let(:output) { Hash[key_primitive.new(:name => 'Test') => value_primitive.new(1)] } it 'coerces keys and values' do # FIXME: expect(subject).to eq(output) crashes in rspec expect(subject.keys.first).to eq(output.keys.first) expect(subject.values.first).to eq(output.values.first) expect(subject.size).to be(1) end end context 'when key type is an array and value type is another hash' do let(:key_primitive) { Array[String] } let(:value_primitive) { Hash[String => Integer] } let(:key_attribute) { Virtus::Attribute.build(key_primitive) } let(:value_attribute) { Virtus::Attribute.build(value_primitive) } let(:input) { Hash[[1, 2], {:one => '1', :two => '2'}] } let(:output) { Hash[key_attribute.coerce(input.keys.first) => value_attribute.coerce(input.values.first)] } it 'coerces keys and values' do expect(subject).to eq(output) end end end end virtus-2.0.0/spec/unit/virtus/attribute/hash/class_methods/0000755000004100000410000000000014072243150024116 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb0000644000004100000410000000551414072243150026561 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute::Hash, '.build' do subject { described_class.build(type, options) } let(:options) { {} } shared_examples_for 'a valid hash attribute instance' do it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it { is_expected.to be_frozen } end context 'when type is Hash' do let(:type) { Hash } it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it 'sets default key type' do expect(subject.type.key_type).to be(Axiom::Types::Object) end it 'sets default value type' do expect(subject.type.value_type).to be(Axiom::Types::Object) end end context 'when type is Hash[String => Integer]' do let(:type) { Hash[String => Integer] } it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it 'sets key type' do expect(subject.type.key_type).to be(Axiom::Types::String) end it 'sets value type' do expect(subject.type.value_type).to be(Axiom::Types::Integer) end end context 'when type is Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean]' do let(:type) { Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean] } it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it 'sets key type' do expect(subject.type.key_type).to be(Axiom::Types::Hash) end it 'sets value type' do expect(subject.type.value_type).to be(Axiom::Types::Boolean) end end context 'when type is Hash[Struct.new(:id) => Integer]' do let(:type) { Hash[key_type => Integer] } let(:key_type) { Struct.new(:id) } it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it 'sets key type' do expect(subject.type.key_type).to be(key_type) end it 'sets value type' do expect(subject.type.value_type).to be(Axiom::Types::Integer) end end context 'when type is Hash[String => Struct.new(:id)]' do let(:type) { Hash[String => value_type] } let(:value_type) { Struct.new(:id) } it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } it 'sets key type' do expect(subject.type.key_type).to be(Axiom::Types::String) end it 'sets value type' do expect(subject.type.value_type).to be(value_type) end end context 'when type is Hash[String => Integer, Integer => String]' do let(:type) { Hash[String => Integer, :Integer => :String] } specify do expect { subject }.to raise_error( ArgumentError, "more than one [key => value] pair in `#{type}`" ) end end context 'when strict mode is used' do let(:type) { Hash[String => Integer] } let(:options) { { :strict => true } } it 'sets the strict mode for key/value types' do expect(subject.key_type).to be_strict expect(subject.value_type).to be_strict end end end virtus-2.0.0/spec/unit/virtus/attribute/lazy_predicate_spec.rb0000644000004100000410000000063514072243150024705 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#lazy?' do subject { object.lazy? } let(:object) { described_class.build(String, options) } let(:options) { Hash[:lazy => lazy] } context 'when :lazy is set to true' do let(:lazy) { true } it { is_expected.to be(true) } end context 'when :lazy is set to false' do let(:lazy) { false } it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/defined_spec.rb0000644000004100000410000000101714072243150023277 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#defined?' do subject { object.defined?(instance) } let(:object) { described_class.build(String, :name => name) } let(:model) { Class.new { attr_accessor :test } } let(:name) { :test } let(:instance) { model.new } context 'when the attribute value has not been defined' do it { is_expected.to be(false) } end context 'when the attribute value has been defined' do before { instance.test = nil } it { is_expected.to be(true) } end end virtus-2.0.0/spec/unit/virtus/attribute/coercible_predicate_spec.rb0000644000004100000410000000066314072243150025656 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#coercible?' do subject { object.coercible? } let(:object) { described_class.build(String, options) } let(:options) { Hash[:coerce => coerce] } context 'when :coerce is set to true' do let(:coerce) { true } it { is_expected.to be(true) } end context 'when :coerce is set to false' do let(:coerce) { false } it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/rename_spec.rb0000644000004100000410000000066014072243150023153 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#rename' do subject { object.rename(:bar) } let(:object) { described_class.build(String, :name => :foo, :strict => true) } let(:other) { described_class.build(String, :name => :bar, :strict => true) } describe '#name' do subject { super().name } it { is_expected.to be(:bar) } end it { is_expected.not_to be(object) } it { is_expected.to be_strict } end virtus-2.0.0/spec/unit/virtus/attribute/comparison_spec.rb0000644000004100000410000000140314072243150024052 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#== (defined by including Virtus::Equalizer)' do let(:attribute) { described_class.build(String, :name => :name) } it 'returns true when attributes have same type and options' do equal_attribute = described_class.build(String, :name => :name) expect(attribute == equal_attribute).to be_truthy end it 'returns false when attributes have different type' do different_attribute = described_class.build(Integer, :name => :name) expect(attribute == different_attribute).to be_falsey end it 'returns false when attributes have different options' do different_attribute = described_class.build(Integer, :name => :name_two) expect(attribute == different_attribute).to be_falsey end end virtus-2.0.0/spec/unit/virtus/attribute/get_spec.rb0000644000004100000410000000143014072243150022457 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#get' do subject { object.get(instance) } let(:object) { described_class.build(String, options.update(:name => name)) } let(:model) { Class.new { attr_accessor :test } } let(:name) { :test } let(:instance) { model.new } let(:value) { 'Jane Doe' } let(:options) { {} } context 'with :lazy is set to false' do before do instance.test = value end it { is_expected.to be(value) } end context 'with :lazy is set to true' do let(:options) { { :lazy => true, :default => value } } it { is_expected.to eql(value) } it 'sets default only on first access' do expect(object.get(instance)).to eql(value) expect(object.get(instance)).to be(instance.test) end end end virtus-2.0.0/spec/unit/virtus/attribute/class_methods/0000755000004100000410000000000014072243150023173 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute/class_methods/build_spec.rb0000644000004100000410000001066114072243150025635 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '.build' do subject { described_class.build(type, options.merge(:name => name)) } let(:name) { :test } let(:type) { String } let(:options) { {} } shared_examples_for 'a valid attribute instance' do it { is_expected.to be_instance_of(Virtus::Attribute) } it { is_expected.to be_frozen } end context 'without options' do it_behaves_like 'a valid attribute instance' it { is_expected.to be_coercible } it { is_expected.to be_public_reader } it { is_expected.to be_public_writer } it { is_expected.not_to be_lazy } it 'sets up a coercer' do expect(subject.options[:coerce]).to be(true) expect(subject.coercer).to be_instance_of(Virtus::Attribute::Coercer) end end context 'when name is passed as a string' do let(:name) { 'something' } describe '#name' do subject { super().name } it { is_expected.to be(:something) } end end context 'when coercion is turned off in options' do let(:options) { { :coerce => false } } it_behaves_like 'a valid attribute instance' it { is_expected.not_to be_coercible } end context 'when options specify reader visibility' do let(:options) { { :reader => :private } } it_behaves_like 'a valid attribute instance' it { is_expected.not_to be_public_reader } it { is_expected.to be_public_writer } end context 'when options specify writer visibility' do let(:options) { { :writer => :private } } it_behaves_like 'a valid attribute instance' it { is_expected.to be_public_reader } it { is_expected.not_to be_public_writer } end context 'when options specify lazy accessor' do let(:options) { { :lazy => true } } it_behaves_like 'a valid attribute instance' it { is_expected.to be_lazy } end context 'when options specify strict mode' do let(:options) { { :strict => true } } it_behaves_like 'a valid attribute instance' it { is_expected.to be_strict } end context 'when options specify nullify blank mode' do let(:options) { { :nullify_blank => true } } it_behaves_like 'a valid attribute instance' it { is_expected.to be_nullify_blank } end context 'when type is a string' do let(:type) { 'Integer' } it_behaves_like 'a valid attribute instance' describe '#type' do subject { super().type } it { is_expected.to be(Axiom::Types::Integer) } end end context 'when type is a range' do let(:type) { 0..10 } it_behaves_like 'a valid attribute instance' describe '#type' do subject { super().type } it { is_expected.to be(Axiom::Types.infer(Range)) } end end context 'when type is a symbol of an existing class constant' do let(:type) { :String } it_behaves_like 'a valid attribute instance' describe '#type' do subject { super().type } it { is_expected.to be(Axiom::Types::String) } end end context 'when type is an axiom type' do let(:type) { Axiom::Types::Integer } it_behaves_like 'a valid attribute instance' describe '#type' do subject { super().type } it { is_expected.to be(type) } end end context 'when custom attribute class exists for a given primitive' do let(:type) { Class.new } let(:attribute) { Class.new(Virtus::Attribute) } before do attribute.primitive(type) end it { is_expected.to be_instance_of(attribute) } describe '#type' do subject { super().type } it { is_expected.to be(Axiom::Types::Object) } end end context 'when custom attribute class exists for a given array with member coercion defined' do let(:type) { Class.new(Array)[String] } let(:attribute) { Class.new(Virtus::Attribute) } before do attribute.primitive(type.class) end it { is_expected.to be_instance_of(attribute) } describe '#type' do subject { super().type } it { is_expected.to be < Axiom::Types::Collection } end end context 'when custom collection-like attribute class exists for a given enumerable primitive' do let(:type) { Class.new { include Enumerable } } let(:attribute) { Class.new(Virtus::Attribute::Collection) } before do attribute.primitive(type) end it { is_expected.to be_instance_of(attribute) } describe '#type' do subject { super().type } it { is_expected.to be < Axiom::Types::Collection } end end end virtus-2.0.0/spec/unit/virtus/attribute/class_methods/coerce_spec.rb0000644000004100000410000000116414072243150025774 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '.coerce' do subject { described_class.coerce } after :all do described_class.coerce(true) end context 'with a value' do it 'sets the value and return self' do expect(described_class.coerce(false)).to be(described_class) expect(subject).to be(false) end end context 'when it is set to true' do before do described_class.coerce(true) end it { is_expected.to be(true) } end context 'when it is set to false' do before do described_class.coerce(false) end it { is_expected.to be(false) } end end virtus-2.0.0/spec/unit/virtus/attribute/set_default_value_spec.rb0000644000004100000410000000511414072243150025376 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::Attribute, '#set_default_value' do let(:object) { described_class.build(String, options.merge(:name => name, :default => default)) } let(:model) { Class.new { def name; 'model'; end; attr_reader :test } } let(:name) { :test } let(:instance) { model.new } let(:options) { {} } before { object.set_default_value(instance) } context 'with a nil' do subject { instance } let(:default) { nil } describe '#test' do subject { super().test } it { is_expected.to be(nil) } end describe '#instance_variables' do subject { super().instance_variables } it { is_expected.to include(:'@test') } end end context 'with a non-clonable object' do subject { instance } let(:object) { described_class.build('Boolean', options.merge(:name => name, :default => default)) } let(:default) { true } describe '#test' do subject { super().test } it { is_expected.to be(true) } end describe '#instance_variables' do subject { super().instance_variables } it { is_expected.to include(:'@test') } end end context 'with a clonable' do subject { instance } let(:default) { [] } describe '#test' do subject { super().test } it { is_expected.to eq(default) } end describe '#test' do subject { super().test } it { is_expected.not_to be(default) } end end context 'with a callable' do subject { instance } let(:default) { lambda { |model, attribute| "#{model.name}-#{attribute.name}" } } describe '#test' do subject { super().test } it { is_expected.to eq('model-test') } end end context 'with a symbol' do subject { instance } context 'when it is a method name' do let(:default) { :set_test } context 'when method is public' do let(:model) { Class.new { attr_reader :test; def set_test; @test = 'hello world'; end } } describe '#test' do subject { super().test } it { is_expected.to eq('hello world') } end end context 'when method is private' do let(:model) { Class.new { attr_reader :test; private; def set_test; @test = 'hello world'; end } } describe '#test' do subject { super().test } it { is_expected.to eq('hello world') } end end end context 'when it is not a method name' do let(:default) { :hello_world } describe '#test' do subject { super().test } it { is_expected.to eq('hello_world') } end end end end virtus-2.0.0/spec/unit/virtus/attribute_spec.rb0000644000004100000410000001234614072243150021710 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#attribute' do let(:name) { :test } let(:options) { {} } shared_examples_for 'a class with boolean attribute' do subject { Test } let(:object) { subject.new } it 'defines reader and writer' do object.test = true expect(object).to be_test end it 'defines predicate method' do object.test = false expect(object).to_not be_test end end shared_examples_for 'an object with string attribute' do it { is_expected.to respond_to(:test) } it { is_expected.to respond_to(:test=) } it 'can write and read the attribute' do subject.test = :foo expect(subject.test).to eql('foo') end end it 'returns self' do klass = Class.new { include Virtus } expect(klass.attribute(:test, String)).to be(klass) end it 'raises error when :name is a reserved name on a class' do klass = Class.new { include Virtus } expect { klass.attribute(:attributes, Set) }.to raise_error( ArgumentError, ':attributes is not allowed as an attribute name' ) end it 'raises error when :name is a reserved name on an instance' do object = Class.new.new.extend(Virtus) expect { object.attribute(:attributes, Set) }.to raise_error( ArgumentError, ':attributes is not allowed as an attribute name' ) end it 'allows :attributes as an attribute name when mass-assignment is not included' do klass = Class.new { include Virtus::Model::Core } klass.attribute(:attributes, Set) expect(klass.attribute_set[:attributes]).to be_instance_of(Virtus::Attribute::Collection) end it 'allows specifying attribute without type' do klass = Class.new { include Virtus::Model::Core } klass.attribute(:name) expect(klass.attribute_set[:name]).to be_instance_of(Virtus::Attribute) end context 'with a class' do context 'when type is Boolean' do before :all do class Test include Virtus attribute :test, Boolean end end after :all do Object.send(:remove_const, :Test) end it_behaves_like 'a class with boolean attribute' end context 'when type is "Boolean"' do before :all do class Test include Virtus attribute :test, 'Boolean' end end after :all do Object.send(:remove_const, :Test) end it_behaves_like 'a class with boolean attribute' end context 'when type is Axiom::Types::Boolean' do before :all do class Test include Virtus attribute :test, Axiom::Types::Boolean end end after :all do Object.send(:remove_const, :Test) end it_behaves_like 'a class with boolean attribute' do before do pending 'this will be fixed once Attribute::Boolean subclass is gone' end end end context 'when type is :Boolean' do before :all do class Test include Virtus attribute :test, 'Boolean' end end after :all do Object.send(:remove_const, :Test) end it_behaves_like 'a class with boolean attribute' context 'with a subclass' do it_behaves_like 'a class with boolean attribute' do subject { Class.new(Test) } it 'gets attributes from the parent class' do Test.attribute :other, Integer expect(subject.attribute_set[:other]).to eql(Test.attribute_set[:other]) end end end end context 'when type is Decimal' do before :all do class Test include Virtus attribute :test, Decimal end end after :all do Object.send(:remove_const, :Test) end it 'maps type to the corresponding axiom type' do expect(Test.attribute_set[:test].type).to be(Axiom::Types::Decimal) end end end context 'with a module' do let(:mod) { Module.new { include Virtus attribute :test, String } } let(:model) { Class.new } context 'included in the class' do before do model.send(:include, mod) end it 'adds attributes from the module to a class that includes it' do expect(model.attribute_set[:test]).to be_instance_of(Virtus::Attribute) end it_behaves_like 'an object with string attribute' do subject { model.new } end end context 'included in the class' do it_behaves_like 'an object with string attribute' do subject { model.new.extend(mod) } end end end context 'with an instance' do subject { model.new } let(:model) { Class.new } before do subject.extend(Virtus) subject.attribute(:test, String) end it_behaves_like 'an object with string attribute' end context 'using custom module' do subject { model.new } let(:model) { Class.new { include Virtus.model { |config| config.coerce = false } attribute :test, String } } it { is_expected.to respond_to(:test) } it { is_expected.to respond_to(:test=) } it 'writes and reads attributes' do subject.test = :foo expect(subject.test).to be(:foo) end end end virtus-2.0.0/spec/unit/virtus/config_spec.rb0000644000004100000410000000044014072243150021142 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '.config' do it 'provides global configuration' do Virtus.config { |config| config.coerce = false } expect(Virtus.coerce).to be(false) Virtus.config { |config| config.coerce = true } expect(Virtus.coerce).to be(true) end end virtus-2.0.0/spec/unit/virtus/freeze_spec.rb0000644000004100000410000000156314072243150021164 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#freeze' do subject { object.freeze } let(:model) { Class.new { include Virtus attribute :name, String, :default => 'foo', :lazy => true attribute :age, Integer, :default => 30 attribute :rand, Float, :default => Proc.new { rand } } } let(:object) { model.new } it { is_expected.to be_frozen } describe '#name' do subject { super().name } it { is_expected.to eql('foo') } end describe '#age' do subject { super().age } it { is_expected.to be(30) } end it "does not change dynamic default values" do original_value = object.rand object.freeze expect(object.rand).to eq original_value end it "does not change default attributes that have been explicitly set" do object.rand = 3.14 object.freeze expect(object.rand).to eq 3.14 end end virtus-2.0.0/spec/unit/virtus/attribute_set/0000755000004100000410000000000014072243150021216 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/attribute_set/each_spec.rb0000644000004100000410000000373214072243150023462 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#each' do subject(:attribute_set) { described_class.new(parent, attributes) } let(:name) { :name } let(:attribute) { Virtus::Attribute.build(String, :name => :name) } let(:attributes) { [ attribute ] } let(:parent) { described_class.new } let(:yields) { Set[] } context 'with no block' do it 'returns an enumerator when block is not provided' do expect(attribute_set.each).to be_kind_of(Enumerator) end it 'yields the expected attributes' do result = [] attribute_set.each { |attribute| result << attribute } expect(result).to eql(attributes) end end context 'with a block' do subject { attribute_set.each { |attribute| yields << attribute } } context 'when the parent has no attributes' do it { is_expected.to equal(attribute_set) } it 'yields the expected attributes' do expect { subject }.to change { yields.dup }. from(Set[]). to(attributes.to_set) end end context 'when the parent has attributes that are not duplicates' do let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } let(:parent) { described_class.new([ parent_attribute ]) } it { is_expected.to equal(attribute_set) } it 'yields the expected attributes' do result = [] attribute_set.each { |attribute| result << attribute } expect(result).to eql([parent_attribute, attribute]) end end context 'when the parent has attributes that are duplicates' do let(:parent_attribute) { Virtus::Attribute.build(String, :name => name) } let(:parent) { described_class.new([ parent_attribute ]) } it { is_expected.to equal(attribute_set) } it 'yields the expected attributes' do expect { subject }.to change { yields.dup }. from(Set[]). to(Set[ attribute ]) end end end end virtus-2.0.0/spec/unit/virtus/attribute_set/append_spec.rb0000644000004100000410000000264014072243150024026 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#<<' do subject { object << attribute } let(:attributes) { [] } let(:parent) { described_class.new } let(:object) { described_class.new(parent, attributes) } let(:name) { :name } context 'with a new attribute' do let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(object) } it 'adds an attribute' do expect { subject }.to change { object.to_a }. from(attributes). to([ attribute ]) end it 'indexes the new attribute under its #name property' do expect { subject }.to change { object[name] }. from(nil). to(attribute) end it 'indexes the new attribute under the string version of its #name property' do expect { subject }.to change { object[name.to_s] }. from(nil). to(attribute) end end context 'with a duplicate attribute' do let(:attributes) { [Virtus::Attribute.build(String, :name => name)] } let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(object) } it "replaces the original attribute object" do expect { subject }.to change { object.to_a.map(&:__id__) }. from(attributes.map(&:__id__)). to([attribute.__id__]) end end end virtus-2.0.0/spec/unit/virtus/attribute_set/element_reference_spec.rb0000644000004100000410000000113614072243150026225 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#[]' do subject { object[name] } let(:name) { :name } let(:attribute) { Virtus::Attribute.build(String, :name => :name) } let(:attributes) { [ attribute ] } let(:parent) { described_class.new } let(:object) { described_class.new(parent, attributes) } it { is_expected.to equal(attribute) } it 'allows indexed access to attributes by the string representation of their name' do expect(object[name.to_s]).to equal(attribute) end end virtus-2.0.0/spec/unit/virtus/attribute_set/element_set_spec.rb0000644000004100000410000000414014072243150025060 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#[]=' do subject { object[name] = attribute } let(:attributes) { [] } let(:parent) { described_class.new } let(:object) { described_class.new(parent, attributes) } let(:name) { :name } context 'with a new attribute' do let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(attribute) } it 'adds an attribute' do expect { subject }.to change { object.to_a }.from(attributes).to([ attribute ]) end it 'allows #[] to access the attribute with a symbol' do expect { subject }.to change { object['name'] }.from(nil).to(attribute) end it 'allows #[] to access the attribute with a string' do expect { subject }.to change { object[:name] }.from(nil).to(attribute) end it 'allows #reset to track overridden attributes' do expect { subject }.to change { object.reset.to_a }.from(attributes).to([ attribute ]) end end context 'with a duplicate attribute' do let(:original) { Virtus::Attribute.build(String, :name => name) } let(:attributes) { [ original ] } let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(attribute) } it "replaces the original attribute object" do expect { subject }.to change { object.to_a.map(&:__id__) }. from(attributes.map(&:__id__)). to([attribute.__id__]) end it 'allows #[] to access the attribute with a string' do expect { subject }.to change { object['name'].__id__ }. from(original.__id__). to(attribute.__id__) end it 'allows #[] to access the attribute with a symbol' do expect { subject }.to change { object[:name].__id__ }. from(original.__id__). to(attribute.__id__) end it 'allows #reset to track overridden attributes' do expect { subject }.to change { object.reset.to_a.map(&:__id__) }. from(attributes.map(&:__id__)). to([attribute.__id__]) end end end virtus-2.0.0/spec/unit/virtus/attribute_set/define_writer_method_spec.rb0000644000004100000410000000166214072243150026750 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#define_writer_method' do subject(:attribute_set) { described_class.new } let(:attribute) { Virtus::Attribute.build(String, :name => method_name) } let(:method_name) { :foo_bar } before do attribute_set.define_writer_method(attribute, method_name, visibility) end context "with public visibility" do let(:visibility) { :public } it "defines public writer" do expect(attribute_set.public_instance_methods).to include(method_name) end end context "with private visibility" do let(:visibility) { :private } it "defines private writer" do expect(attribute_set.private_instance_methods).to include(method_name) end end context "with protected visibility" do let(:visibility) { :protected } it "defines protected writer" do expect(attribute_set.protected_instance_methods).to include(method_name) end end end virtus-2.0.0/spec/unit/virtus/attribute_set/reset_spec.rb0000644000004100000410000000416514072243150023705 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#reset' do subject { object.reset } let(:name) { :name } let(:attribute) { Virtus::Attribute.build(String, :name => :name) } let(:attributes) { [ attribute ] } let(:object) { described_class.new(parent, attributes) } context 'when the parent has no attributes' do let(:parent) { described_class.new } it { is_expected.to equal(object) } describe '#to_set' do subject { super().to_set } it { is_expected.to eq(Set[ attribute ]) } end end context 'when the parent has attributes that are not duplicates' do let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } let(:parent) { described_class.new([ parent_attribute ]) } it { is_expected.to equal(object) } describe '#to_set' do subject { super().to_set } it { is_expected.to eq(Set[ attribute, parent_attribute ]) } end end context 'when the parent has attributes that are duplicates' do let(:parent_attribute) { Virtus::Attribute.build(String, :name => name) } let(:parent) { described_class.new([ parent_attribute ]) } it { is_expected.to equal(object) } describe '#to_set' do subject { super().to_set } it { is_expected.to eq(Set[ attribute ]) } end end context 'when the parent has changed' do let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } let(:parent) { described_class.new([ parent_attribute ]) } let(:new_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } it { is_expected.to equal(object) } it 'includes changes from the parent' do expect(object.to_set).to eql(Set[attribute, parent_attribute]) parent << new_attribute expect(subject.to_set).to eql(Set[attribute, new_attribute]) end end context 'when the parent is nil' do let(:parent) { nil } it { is_expected.to equal(object) } it 'includes changes from the parent' do expect { subject }.to_not change { object.to_set } end end end virtus-2.0.0/spec/unit/virtus/attribute_set/define_reader_method_spec.rb0000644000004100000410000000166114072243150026675 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#define_reader_method' do subject(:attribute_set) { described_class.new } let(:attribute) { Virtus::Attribute.build(String, :name => method_name) } let(:method_name) { :foo_bar } before do attribute_set.define_reader_method(attribute, method_name, visibility) end context "with public visibility" do let(:visibility) { :public } it "defines public writer" do expect(attribute_set.public_instance_methods).to include(method_name) end end context "with private visibility" do let(:visibility) { :private } it "defines public writer" do expect(attribute_set.private_instance_methods).to include(method_name) end end context "with protected visibility" do let(:visibility) { :protected } it "defines protected writer" do expect(attribute_set.protected_instance_methods).to include(method_name) end end end virtus-2.0.0/spec/unit/virtus/attribute_set/merge_spec.rb0000644000004100000410000000174414072243150023662 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::AttributeSet, '#merge' do subject { object.merge(other) } let(:parent) { described_class.new } let(:object) { described_class.new(parent, attributes) } let(:name) { :name } let(:other) { [attribute] } context 'with a new attribute' do let(:attributes) { [] } let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(object) } it 'adds an attribute' do expect { subject }.to change { object.to_a }.from(attributes).to([attribute]) end end context 'with a duplicate attribute' do let(:attributes) { [Virtus::Attribute.build(String, :name => name)] } let(:attribute) { Virtus::Attribute.build(String, :name => name) } it { is_expected.to equal(object) } it "replaces the original attribute object" do expect { subject }.to change { object.to_a.map(&:__id__) }. from(attributes.map(&:__id__)). to([attribute.__id__]) end end end virtus-2.0.0/spec/unit/virtus/module_spec.rb0000644000004100000410000001164414072243150021172 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '.module' do shared_examples_for 'a valid virtus object' do it 'reads and writes attribute' do instance.name = 'John' expect(instance.name).to eql('John') end end shared_examples_for 'an object extended with virtus module' do context 'with default configuration' do subject { Virtus.module } it_behaves_like 'a valid virtus object' do let(:instance) { model.new } end it 'sets defaults' do expect(instance.name).to eql('Jane') end end context 'with constructor turned off' do subject { Virtus.module(:constructor => false) } it_behaves_like 'a valid virtus object' do let(:instance) { model.new } end it 'skips including constructor' do expect { model.new({}) }.to raise_error(ArgumentError) end end context 'with mass assignment is turned off' do subject { Virtus.module(:mass_assignment => false) } it_behaves_like 'a valid virtus object' it 'skips including mass assignment' do expect(instance).not_to respond_to(:attributes) expect(instance).not_to respond_to(:attributes=) end end context 'with coercion turned off' do subject { Virtus.module(:coerce => false) } it_behaves_like 'a valid virtus object' it 'builds non-coercible attributes' do expect(object.send(:attribute_set)[:name]).not_to be_coercible end end end let(:mod) { Module.new } let(:model) { Class.new } let(:instance) { model.new } before do mod.send(:include, subject) mod.attribute :name, String, :default => 'Jane' mod.attribute :something end context 'with a class' do let(:object) { model } before do model.send(:include, mod) end it 'provides attributes for the model' do expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) end it 'defaults to Object for attribute type' do expect(model.attribute_set[:something].type).to be(Axiom::Types::Object) end it_behaves_like 'an object extended with virtus module' end context 'with a model instance' do let(:object) { instance } before do instance.extend(mod) end it 'provides attributes for the instance' do expect(instance.send(:attribute_set)[:name]).to be_kind_of(Virtus::Attribute) end it_behaves_like 'an object extended with virtus module' end context 'with another module' do let(:other) { Module.new } let(:object) { instance } before do other.send(:include, mod) model.send(:include, other) end it_behaves_like 'an object extended with virtus module' it 'provides attributes for the model' do expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) end end context 'as a peer to another module within a class' do subject { Virtus.module } let(:other) { Module.new } before do other.send(:include, Virtus.module) other.attribute :last_name, String, :default => 'Doe' other.attribute :something_else model.send(:include, mod) model.send(:include, other) end it 'provides attributes for the model from both modules' do expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:something]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:last_name]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:something_else]).to be_kind_of(Virtus::Attribute) end it 'includes the attributes from both modules' do expect(model.new.attributes.keys).to eq( [:name, :something, :last_name, :something_else] ) end end context 'with multiple other modules mixed into it' do subject { Virtus.module } let(:other) { Module.new } let(:yet_another) { Module.new } before do other.send(:include, Virtus.module) other.attribute :last_name, String, :default => 'Doe' other.attribute :something_else yet_another.send(:include, Virtus.module) yet_another.send(:include, mod) yet_another.send(:include, other) yet_another.attribute :middle_name, String, :default => 'Foobar' model.send(:include, yet_another) end it 'provides attributes for the model from all modules' do expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:something]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:last_name]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:something_else]).to be_kind_of(Virtus::Attribute) expect(model.attribute_set[:middle_name]).to be_kind_of(Virtus::Attribute) end it 'includes the attributes from all modules' do expect(model.new.attributes.keys).to eq( [:name, :something, :last_name, :something_else, :middle_name] ) end end end virtus-2.0.0/spec/unit/virtus/class_methods/0000755000004100000410000000000014072243150021170 5ustar www-datawww-datavirtus-2.0.0/spec/unit/virtus/class_methods/finalize_spec.rb0000644000004100000410000000374114072243150024335 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '.finalize' do before do module Examples class Person include Virtus.model(:finalize => false) attribute :name, String attribute :articles, Array['Examples::Article'] attribute :address, :'Examples::Address' end class Article include Virtus.model(:finalize => false) attribute :posts, Hash['Examples::Person' => 'Examples::Post'] attribute :person, :'Examples::Person' end class Post include Virtus.model attribute :title, String end class Address include Virtus.model attribute :street, String end end expect(Examples::Post.attribute_set[:title]).to be_finalized expect(Examples::Address.attribute_set[:street]).to be_finalized expect(Virtus::Builder.pending).not_to include(Examples::Post) expect(Virtus::Builder.pending).not_to include(Examples::Address) Virtus.finalize end it "sets attributes that don't require finalization" do expect(Examples::Person.attribute_set[:name]).to be_instance_of(Virtus::Attribute) expect(Examples::Person.attribute_set[:name].primitive).to be(String) end it 'it finalizes member type for a collection attribute' do expect(Examples::Person.attribute_set[:address].primitive).to be(Examples::Address) end it 'it finalizes key type for a hash attribute' do expect(Examples::Article.attribute_set[:posts].key_type.primitive).to be(Examples::Person) end it 'it finalizes value type for a hash attribute' do expect(Examples::Article.attribute_set[:posts].value_type.primitive).to be(Examples::Post) end it 'it finalizes type for an EV attribute' do expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person) end it 'automatically resolves constant when it is already available' do expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person) end end virtus-2.0.0/spec/unit/virtus/class_methods/new_spec.rb0000644000004100000410000000176414072243150023330 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '.new' do let(:model) { Class.new { include Virtus attribute :id, Integer attribute :name, String, :default => 'John Doe' attribute :email, String, :default => 'john@doe.com', :lazy => true, :writer => :private } } context 'without attribute hash' do subject { model.new } it 'sets default values for non-lazy attributes' do expect(subject.instance_variable_get('@name')).to eql('John Doe') end it 'skips setting default values for lazy attributes' do expect(subject.instance_variable_get('@email')).to be(nil) end end context 'with attribute hash' do subject { model.new(:id => 1, :name => 'Jane Doe') } it 'sets attributes with public writers' do expect(subject.id).to be(1) expect(subject.name).to eql('Jane Doe') end it 'skips setting attributes with private writers' do expect(subject.instance_variable_get('@email')).to be(nil) end end end virtus-2.0.0/spec/unit/virtus/element_writer_spec.rb0000644000004100000410000000046014072243150022724 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#[]=' do subject { object[:test] = 'foo' } let(:model) { Class.new { include Virtus attribute :test, String } } let(:object) { model.new } specify do expect { subject }.to change { object.test }.from(nil).to('foo') end end virtus-2.0.0/spec/unit/virtus/attributes_writer_spec.rb0000644000004100000410000000212614072243150023462 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, '#attributes=' do shared_examples_for 'mass-assignment' do it 'allows writing known attributes' do subject.attributes = { :test => 'Hello World' } expect(subject.test).to eql('Hello World') end it 'skips writing unknown attributes' do subject.attributes = { :test => 'Hello World', :nothere => 'boom!' } expect(subject.test).to eql('Hello World') end end context 'with a class' do let(:model) { Class.new { include Virtus attribute :test, String } } it_behaves_like 'mass-assignment' do subject { model.new } end it 'raises when attributes is not hash-like object' do expect { model.new('not a hash, really') }.to raise_error( NoMethodError, 'Expected "not a hash, really" to respond to #to_hash' ) end end context 'with an instance' do subject { model.new } let(:model) { Class.new } before do subject.extend(Virtus) subject.attribute :test, String end it_behaves_like 'mass-assignment' end end virtus-2.0.0/spec/spec_helper.rb0000644000004100000410000000175414072243150016652 0ustar www-datawww-dataif RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '3.0' require 'simplecov' SimpleCov.start end require 'rspec' require 'bogus/rspec' require 'virtus' module Virtus def self.warn(*) # shut up in tests end end ENV['TZ'] = 'UTC' # require spec support files and shared behavior Dir[File.expand_path('../shared/**/*.rb', __FILE__)].each { |file| require file } RSpec.configure do |config| # Remove anonymous- and example- Attribute classes from Attribute descendants config.after :all do stack = [ Virtus::Attribute ] while klass = stack.pop klass.descendants.delete_if do |descendant| descendant.name.nil? || descendant.name.empty? || descendant.name.start_with?('Examples::') end stack.concat(klass.descendants) end end # Remove constants in the Example-Module config.after :each do if defined?(Examples) Examples.constants.each do |const_name| ConstantsHelpers.undef_constant(Examples, const_name) end end end end virtus-2.0.0/spec/integration/0000755000004100000410000000000014072243150016350 5ustar www-datawww-datavirtus-2.0.0/spec/integration/custom_attributes_spec.rb0000644000004100000410000000150214072243150023465 0ustar www-datawww-datarequire 'spec_helper' describe 'custom attributes' do before do module Examples class NoisyString < Virtus::Attribute lazy true def coerce(input) input.to_s.upcase end end class RegularExpression < Virtus::Attribute primitive Regexp end class User include Virtus attribute :name, String attribute :scream, NoisyString attribute :expression, RegularExpression end end end subject { Examples::User.new } specify 'allows you to define custom attributes' do regexp = /awesome/ subject.expression = regexp expect(subject.expression).to eq(regexp) end specify 'allows you to define coercion methods' do subject.scream = 'welcome' expect(subject.scream).to eq('WELCOME') end end virtus-2.0.0/spec/integration/extending_objects_spec.rb0000644000004100000410000000131614072243150023406 0ustar www-datawww-datarequire 'spec_helper' describe 'I can extend objects' do before do module Examples class User; end class Admin; end end end specify 'defining attributes on an object' do attributes = { :name => 'John', :age => 29 } admin = Examples::Admin.new admin.extend(Virtus) admin.attribute :name, String admin.attribute :age, Integer admin.name = 'John' admin.age = 29 expect(admin.name).to eql('John') expect(admin.age).to eql(29) expect(admin.attributes).to eql(attributes) new_attributes = { :name => 'Jane', :age => 28 } admin.attributes = new_attributes expect(admin.name).to eql('Jane') expect(admin.age).to eql(28) end end virtus-2.0.0/spec/integration/injectible_coercers_spec.rb0000644000004100000410000000211714072243150023705 0ustar www-datawww-datarequire 'virtus' describe 'Injectible coercer' do before do module Examples class EmailAddress include Virtus.value_object values do attribute :address, String, :coercer => lambda { |add| add.downcase } end def self.coerce(input) if input.is_a?(String) new(:address => input) else new(input) end end end class User include Virtus.model attribute :email, EmailAddress, :coercer => lambda { |input| Examples::EmailAddress.coerce(input) } end end end after do Examples.send(:remove_const, :EmailAddress) Examples.send(:remove_const, :User) end let(:doe) { Examples::EmailAddress.new(:address => 'john.doe@example.com') } it 'accepts an email hash' do user = Examples::User.new :email => { :address => 'John.Doe@Example.Com' } expect(user.email).to eq(doe) end it 'coerces an embedded string' do user = Examples::User.new :email => 'John.Doe@Example.Com' expect(user.email).to eq(doe) end end virtus-2.0.0/spec/integration/default_values_spec.rb0000644000004100000410000000444414072243150022720 0ustar www-datawww-datarequire 'spec_helper' describe "default values" do before do module Examples class Reference include Virtus::ValueObject attribute :ref, String end class Page include Virtus attribute :title, String attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }, :lazy => true attribute :view_count, Integer, :default => 0 attribute :published, Boolean, :default => false, :accessor => :private attribute :editor_title, String, :default => :default_editor_title, :lazy => true attribute :reference, String, :default => Reference.new attribute :revisions, Array attribute :index, Hash attribute :authors, Set def default_editor_title published? ? title : "UNPUBLISHED: #{title}" end end end end subject { Examples::Page.new } specify 'without a default the value is nil' do expect(subject.title).to be_nil end specify 'can be supplied with the :default option' do expect(subject.view_count).to eq(0) end specify "you can pass a 'callable-object' to the :default option" do subject.title = 'Example Blog Post' expect(subject.slug).to eq('example-blog-post') end specify 'you can set defaults for private attributes' do subject.title = 'Top Secret' expect(subject.editor_title).to eq('UNPUBLISHED: Top Secret') end specify 'you can reset attribute to its default' do subject.view_count = 10 expect do subject.reset_attribute(:view_count) end.to change { subject.view_count }.to(0) end context 'a ValueObject' do it 'does not duplicate the ValueObject' do page1 = Examples::Page.new page2 = Examples::Page.new expect(page1.reference).to equal(page2.reference) end end context 'an Array' do specify 'without a default the value is an empty Array' do expect(subject.revisions).to eql([]) end end context 'a Hash' do specify 'without a default the value is an empty Hash' do expect(subject.index).to eql({}) end end context 'a Set' do specify 'without a default the value is an empty Set' do expect(subject.authors).to eql(Set.new) end end end virtus-2.0.0/spec/integration/struct_as_embedded_value_spec.rb0000644000004100000410000000117714072243150024731 0ustar www-datawww-datarequire 'spec_helper' describe 'Using Struct as an embedded value attribute' do before do module Examples Point = Struct.new(:x, :y) class Rectangle include Virtus attribute :top_left, Point attribute :bottom_right, Point end end end subject do Examples::Rectangle.new(:top_left => [ 3, 5 ], :bottom_right => [ 8, 7 ]) end specify 'initialize a struct object with correct attributes' do expect(subject.top_left.x).to be(3) expect(subject.top_left.y).to be(5) expect(subject.bottom_right.x).to be(8) expect(subject.bottom_right.y).to be(7) end end virtus-2.0.0/spec/integration/inheritance_spec.rb0000644000004100000410000000203614072243150022201 0ustar www-datawww-datarequire 'spec_helper' describe 'Inheritance' do before do module Examples class Base include Virtus.model end class First < Base attribute :id, Fixnum attribute :name, String, default: ->(first, _) { "Named: #{first.id}" } attribute :description, String end class Second < Base attribute :something, String end end end it 'inherits model from the base class' do expect(Examples::First.attribute_set.map(&:name)).to eql([:id, :name, :description]) expect(Examples::Second.attribute_set.map(&:name)).to eql([:something]) end it 'sets correct attributes on the descendant classes' do first = Examples::First.new(:id => 1, :description => 'hello world') expect(first.id).to be(1) expect(first.name).to eql('Named: 1') expect(first.description).to eql('hello world') second = Examples::Second.new expect(second.something).to be(nil) second.something = 'foo bar' expect(second.something).to eql('foo bar') end end virtus-2.0.0/spec/integration/collection_member_coercion_spec.rb0000644000004100000410000000433614072243150025260 0ustar www-datawww-datarequire 'spec_helper' # TODO: refactor to make it inline with the new style of integration specs class Address include Virtus attribute :address, String attribute :locality, String attribute :region, String attribute :postal_code, String end class PhoneNumber include Virtus attribute :number, String end class User include Virtus attribute :phone_numbers, Array[PhoneNumber] attribute :addresses, Set[Address] end describe User do it { is_expected.to respond_to(:phone_numbers) } it { is_expected.to respond_to(:phone_numbers=) } it { is_expected.to respond_to(:addresses) } it { is_expected.to respond_to(:addresses=) } let(:instance) do described_class.new(:phone_numbers => phone_numbers_attributes, :addresses => addresses_attributes) end let(:phone_numbers_attributes) { [ { :number => '212-555-1212' }, { :number => '919-444-3265' }, ] } let(:addresses_attributes) { [ { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" }, ] } describe '#phone_numbers' do describe 'first entry' do subject { instance.phone_numbers.first } it { is_expected.to be_instance_of(PhoneNumber) } describe '#number' do subject { super().number } it { is_expected.to eql('212-555-1212') } end end describe 'last entry' do subject { instance.phone_numbers.last } it { is_expected.to be_instance_of(PhoneNumber) } describe '#number' do subject { super().number } it { is_expected.to eql('919-444-3265') } end end end describe '#addresses' do subject { instance.addresses.first } it { is_expected.to be_instance_of(Address) } describe '#address' do subject { super().address } it { is_expected.to eql('1234 Any St.') } end describe '#locality' do subject { super().locality } it { is_expected.to eql('Anytown') } end describe '#region' do subject { super().region } it { is_expected.to eql('DC') } end describe '#postal_code' do subject { super().postal_code } it { is_expected.to eql('21234') } end end end virtus-2.0.0/spec/integration/embedded_value_spec.rb0000644000004100000410000000224714072243150022641 0ustar www-datawww-datarequire 'spec_helper' describe 'embedded values' do before do module Examples class City include Virtus.model attribute :name, String end class Address include Virtus.model attribute :street, String attribute :zipcode, String attribute :city, City end class User include Virtus.model attribute :name, String attribute :address, Address end end end subject { Examples::User.new(:name => 'the guy', :address => address_attributes) } let(:address_attributes) do { :street => 'Street 1/2', :zipcode => '12345', :city => { :name => 'NYC' } } end specify '#attributes returns instances of the embedded values' do expect(subject.attributes).to eq({ :name => 'the guy', :address => subject.address }) end specify 'allows you to pass a hash for the embedded value' do user = Examples::User.new user.address = address_attributes expect(user.address.street).to eq('Street 1/2') expect(user.address.zipcode).to eq('12345') expect(user.address.city.name).to eq('NYC') end end virtus-2.0.0/spec/integration/value_object_with_custom_constructor_spec.rb0000644000004100000410000000167314072243150027452 0ustar www-datawww-datarequire 'spec_helper' describe "Defining a ValueObject with a custom constructor" do before do module Examples class Point include Virtus::ValueObject attribute :x, Integer attribute :y, Integer def initialize(attributes) if attributes.kind_of?(Array) self.x = attributes.first self.y = attributes.last else super end end end class Rectangle include Virtus attribute :top_left, Point attribute :bottom_right, Point end end end subject do Examples::Rectangle.new(:top_left => [ 3, 4 ], :bottom_right => [ 5, 8 ]) end specify "initialize a value object attribute with correct attributes" do expect(subject.top_left.x).to be(3) expect(subject.top_left.y).to be(4) expect(subject.bottom_right.x).to be(5) expect(subject.bottom_right.y).to be(8) end end virtus-2.0.0/spec/integration/mass_assignment_with_accessors_spec.rb0000644000004100000410000000201414072243150026177 0ustar www-datawww-datarequire 'spec_helper' describe "mass assignment with accessors" do before do module Examples class Product include Virtus attribute :id, Integer attribute :category, String attribute :subcategory, String def categories=(categories) self.category = categories.first self.subcategory = categories.last end private def _id=(value) self.id = value end end end end subject { Examples::Product.new(:categories => ['Office', 'Printers'], :_id => 100) } specify 'works uppon instantiation' do expect(subject.category).to eq('Office') expect(subject.subcategory).to eq('Printers') end specify 'can be set with #attributes=' do subject.attributes = {:categories => ['Home', 'Furniture']} expect(subject.category).to eq('Home') expect(subject.subcategory).to eq('Furniture') end specify 'respects accessor visibility' do expect(subject.id).not_to eq(100) end end virtus-2.0.0/spec/integration/building_module_spec.rb0000644000004100000410000000402714072243150023054 0ustar www-datawww-datarequire 'spec_helper' describe 'I can create a Virtus module' do before do module Examples NoncoercingModule = Virtus.model { |config| config.coerce = false } CoercingModule = Virtus.model { |config| config.coerce = true config.coercer do |coercer| coercer.string.boolean_map = { 'yup' => true, 'nope' => false } end } StrictModule = Virtus.model { |config| config.strict = true } BlankModule = Virtus.model { |config| config.nullify_blank = true } class NoncoercedUser include NoncoercingModule attribute :name, String attribute :happy, String end class CoercedUser include CoercingModule attribute :name, String attribute :happy, Boolean end class StrictModel include StrictModule attribute :stuff, Hash attribute :happy, Boolean, :strict => false end class BlankModel include BlankModule attribute :stuff, Hash attribute :happy, Boolean, :nullify_blank => false end end end specify 'including a custom module with coercion disabled' do user = Examples::NoncoercedUser.new(:name => :Giorgio, :happy => 'yes') expect(user.name).to be(:Giorgio) expect(user.happy).to eql('yes') end specify 'including a custom module with coercion enabled' do user = Examples::CoercedUser.new(:name => 'Paul', :happy => 'nope') expect(user.name).to eql('Paul') expect(user.happy).to be(false) end specify 'including a custom module with strict enabled' do model = Examples::StrictModel.new expect { model.stuff = 'foo' }.to raise_error(Virtus::CoercionError) model.happy = 'foo' expect(model.happy).to eql('foo') end specify 'including a custom module with nullify blank enabled' do model = Examples::BlankModel.new model.stuff = '' expect(model.stuff).to be_nil model.happy = 'foo' expect(model.happy).to eql('foo') end end virtus-2.0.0/spec/integration/using_modules_spec.rb0000644000004100000410000000254014072243150022565 0ustar www-datawww-datarequire 'spec_helper' describe 'I can define attributes within a module' do before do module Examples module Common include Virtus end module Name include Common attribute :name, String attribute :gamer, Boolean end module Age include Common attribute :age, Integer end class User include Name end class Admin < User include Age end class Moderator; end end end specify 'including a module with attributes into a class' do expect(Examples::User.attribute_set[:name]).to be_instance_of(Virtus::Attribute) expect(Examples::User.attribute_set[:gamer]).to be_instance_of(Virtus::Attribute::Boolean) expect(Examples::Admin.attribute_set[:name]).to be_instance_of(Virtus::Attribute) expect(Examples::Admin.attribute_set[:age]).to be_instance_of(Virtus::Attribute) user = Examples::Admin.new(:name => 'Piotr', :age => 29) expect(user.name).to eql('Piotr') expect(user.age).to eql(29) end specify 'including a module with attributes into an instance' do moderator = Examples::Moderator.new moderator.extend(Examples::Name, Examples::Age) moderator.attributes = { :name => 'John', :age => 21 } expect(moderator.name).to eql('John') expect(moderator.age).to eql(21) end end virtus-2.0.0/spec/integration/required_attributes_spec.rb0000644000004100000410000000127214072243150023777 0ustar www-datawww-datarequire 'spec_helper' describe 'Using required attributes' do before do module Examples class User include Virtus.model(:strict => true) attribute :name, String attribute :age, Integer, :required => false end end end it 'raises coercion error when required attribute is nil' do expect { Examples::User.new(:name => nil) }.to raise_error(Virtus::CoercionError, "Failed to coerce attribute `name' from nil into String") end it 'does not raise coercion error when not required attribute is nil' do user = Examples::User.new(:name => 'Jane', :age => nil) expect(user.name).to eql('Jane') expect(user.age).to be(nil) end end virtus-2.0.0/spec/integration/defining_attributes_spec.rb0000644000004100000410000000453014072243150023742 0ustar www-datawww-datarequire 'spec_helper' describe "virtus attribute definitions" do before do module Examples class Person include Virtus attribute :name, String attribute :age, Integer attribute :doctor, Boolean attribute :salary, Decimal end class Manager < Person end end end subject(:person) { Examples::Person.new(attributes) } let(:attributes) { {} } specify 'virtus creates accessor methods' do person.name = 'Peter' expect(person.name).to eq('Peter') end specify 'the constructor accepts a hash for mass-assignment' do john = Examples::Person.new(:name => 'John', :age => 13) expect(john.name).to eq('John') expect(john.age).to eq(13) end specify 'Boolean attributes have a predicate method' do expect(person).not_to be_doctor person.doctor = true expect(person).to be_doctor end context 'with attributes' do let(:attributes) { {:name => 'Jane', :age => 45, :doctor => true, :salary => 4500} } specify "#attributes returns the object's attributes as a hash" do expect(person.attributes).to eq(attributes) end specify "#to_hash returns the object's attributes as a hash" do expect(person.to_hash).to eq(attributes) end specify "#to_h returns the object's attributes as a hash" do expect(person.to_h).to eql(attributes) end end context 'inheritance' do specify 'inherits all the attributes from the base class' do fred = Examples::Manager.new(:name => 'Fred', :age => 29) expect(fred.name).to eq('Fred') expect(fred.age).to eq(29) end specify 'lets you add attributes to the base class at runtime' do frank = Examples::Manager.new(:name => 'Frank') Examples::Person.attribute :just_added, String frank.just_added = 'it works!' expect(frank.just_added).to eq('it works!') end specify 'lets you add attributes to the subclass at runtime' do person_jack = Examples::Person.new(:name => 'Jack') manager_frank = Examples::Manager.new(:name => 'Frank') Examples::Manager.attribute :just_added, String manager_frank.just_added = 'awesome!' expect(manager_frank.just_added).to eq('awesome!') expect(person_jack).not_to respond_to(:just_added) expect(person_jack).not_to respond_to(:just_added=) end end end virtus-2.0.0/spec/integration/virtus/0000755000004100000410000000000014072243150017704 5ustar www-datawww-datavirtus-2.0.0/spec/integration/virtus/value_object_spec.rb0000644000004100000410000000606514072243150023714 0ustar www-datawww-datarequire 'spec_helper' describe Virtus::ValueObject do let(:class_under_test) do Class.new do def self.name 'GeoLocation' end include Virtus::ValueObject attribute :latitude, Float attribute :longitude, Float end end let(:attribute_values) { { :latitude => 10.0, :longitude => 20.0 } } let(:instance_with_equal_state) { class_under_test.new(attribute_values) } let(:instance_with_different_state) do class_under_test.new(:latitude => attribute_values[:latitude]) end subject { class_under_test.new(attribute_values) } describe 'initialization' do it 'sets the attribute values provided to Class.new' do expect(class_under_test.new(:latitude => 10000.001).latitude).to eq(10000.001) expect(subject.latitude).to eql(attribute_values[:latitude]) end end describe 'writer visibility' do it 'attributes are configured for private writers' do expect(class_under_test.attribute_set[:latitude].public_reader?).to be(true) expect(class_under_test.attribute_set[:longitude].public_writer?).to be(false) end it 'writer methods are set to private' do private_methods = class_under_test.private_instance_methods private_methods.map! { |m| m.to_s } expect(private_methods).to include('latitude=', 'longitude=', 'attributes=') end it 'attempts to call attribute writer methods raises NameError' do expect { subject.latitude = 5.0 }.to raise_exception(NameError) expect { subject.longitude = 5.0 }.to raise_exception(NameError) end end describe 'equality' do describe '#==' do it 'returns true for different objects with the same state' do expect(subject).to eq(instance_with_equal_state) end it 'returns false for different objects with different state' do expect(subject).not_to eq(instance_with_different_state) end end describe '#eql?' do it 'returns true for different objects with the same state' do expect(subject).to eql(instance_with_equal_state) end it 'returns false for different objects with different state' do expect(subject).not_to eql(instance_with_different_state) end end describe '#equal?' do it 'returns false for different objects with the same state' do expect(subject).not_to equal(instance_with_equal_state) end it 'returns false for different objects with different state' do expect(subject).not_to equal(instance_with_different_state) end end describe '#hash' do it 'returns the same value for different objects with the same state' do expect(subject.hash).to eql(instance_with_equal_state.hash) end it 'returns different values for different objects with different state' do expect(subject.hash).not_to eql(instance_with_different_state.hash) end end end describe '#inspect' do it 'includes the class name and attribute values' do expect(subject.inspect).to eq('#') end end end virtus-2.0.0/spec/integration/virtus/instance_level_attributes_spec.rb0000644000004100000410000000077214072243150026512 0ustar www-datawww-datarequire 'spec_helper' describe Virtus, 'instance level attributes' do subject do subject = Object.new subject.singleton_class.send(:include, Virtus) subject end let(:attribute) { subject.singleton_class.attribute(:name, String) } before do pending if RUBY_VERSION < '1.9' end context 'adding an attribute' do it 'allows setting the attribute value on the instance' do attribute subject.name = 'foo' expect(subject.name).to eql('foo') end end end virtus-2.0.0/spec/integration/overriding_virtus_spec.rb0000644000004100000410000000230114072243150023467 0ustar www-datawww-datarequire 'spec_helper' describe 'overriding virtus behavior' do before do module Examples class Article include Virtus attribute :title, String def title super || '' end def title=(name) super unless self.title == "can't be changed" end end end end describe 'overriding an attribute getter' do specify 'calls the defined getter' do expect(Examples::Article.new.title).to eq('') end specify 'super can be used to access the getter defined by virtus' do expect(Examples::Article.new(:title => 'example article').title).to eq('example article') end end describe 'overriding an attribute setter' do specify 'calls the defined setter' do article = Examples::Article.new(:title => "can't be changed") article.title = 'this will never be assigned' expect(article.title).to eq("can't be changed") end specify 'super can be used to access the setter defined by virtus' do article = Examples::Article.new(:title => 'example article') article.title = 'my new title' expect(article.title).to eq('my new title') end end end virtus-2.0.0/spec/integration/hash_attributes_coercion_spec.rb0000644000004100000410000000243114072243150024761 0ustar www-datawww-datarequire 'spec_helper' class Package include Virtus attribute :dimensions, Hash[Symbol => Float] attribute :meta_info , Hash[String => String] end describe Package do let(:instance) do described_class.new( :dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 }, :meta_info => { 'from' => :Me , :to => 'You' } ) end let(:dimensions) { instance.dimensions } let(:meta_info) { instance.meta_info } describe '#dimensions' do subject { dimensions } it 'has 3 keys' do expect(subject.keys.size).to eq(3) end it { is_expected.to have_key :width } it { is_expected.to have_key :height } it { is_expected.to have_key :length } it 'should be coerced to [Symbol => Float] format' do expect(dimensions[:width]).to be_eql(2.2) expect(dimensions[:height]).to be_eql(2.0) expect(dimensions[:length]).to be_eql(4.5) end end describe '#meta_info' do subject { meta_info } it 'has 2 keys' do expect(subject.keys.size).to eq(2) end it { is_expected.to have_key 'from' } it { is_expected.to have_key 'to' } it 'should be coerced to [String => String] format' do expect(meta_info['from']).to eq('Me') expect(meta_info['to']).to eq('You') end end end virtus-2.0.0/spec/integration/attributes_attribute_spec.rb0000644000004100000410000000131614072243150024161 0ustar www-datawww-datarequire 'spec_helper' describe "Adding attribute called 'attributes'" do context "when mass assignment is disabled" do before do module Examples class User include Virtus.model(mass_assignment: false) attribute :attributes end end end it "allows model to use `attributes` attribute" do user = Examples::User.new expect(user.attributes).to eq(nil) user.attributes = "attributes string" expect(user.attributes).to eq("attributes string") end it "doesn't accept `attributes` key in initializer" do user = Examples::User.new(attributes: 'attributes string') expect(user.attributes).to eq(nil) end end end virtus-2.0.0/spec/integration/custom_collection_attributes_spec.rb0000644000004100000410000000436514072243150025712 0ustar www-datawww-datarequire 'spec_helper' describe 'custom collection attributes' do let(:library) { Examples::Library.new } let(:books) { library.books } before do module Examples end Examples.const_set 'BookCollection', book_collection_class module Examples class Book include Virtus attribute :title, String end class BookCollectionAttribute < Virtus::Attribute::Collection primitive BookCollection end class Library include Virtus attribute :books, BookCollection[Book] end end end after do [:BookCollectionAttribute, :BookCollection, :Book, :Library].each do |const| Examples.send(:remove_const, const) end end shared_examples_for 'a collection' do it 'can be used as Virtus attributes' do attribute = Examples::Library.attribute_set[:books] expect(attribute).to be_kind_of(Examples::BookCollectionAttribute) end it 'defaults to an empty collection' do books_should_be_an_empty_collection end it 'coerces nil' do library.books = nil books_should_be_an_empty_collection end it 'coerces an empty array' do library.books = [] books_should_be_an_empty_collection end it 'coerces an array of attribute hashes' do library.books = [{ :title => 'Foo' }] expect(books).to be_kind_of(Examples::BookCollection) end it 'coerces its members' do library.books = [{ :title => 'Foo' }] expect(books.count).to eq(1) expect(books.first).to be_kind_of(Examples::Book) end def books_should_be_an_empty_collection expect(books).to be_kind_of(Examples::BookCollection) expect(books.count).to eq(0) end end context 'with an array subclass' do let(:book_collection_class) { Class.new(Array) } it_behaves_like 'a collection' end context 'with an enumerable' do require 'forwardable' let(:book_collection_class) { Class.new do extend Forwardable include Enumerable def_delegators :@array, :each, :<< def initialize(*args) @array = Array[*args] end def self.[](*args) new(*args) end end } it_behaves_like 'a collection' end end virtus-2.0.0/TODO.md0000644000004100000410000000035714072243150014167 0ustar www-datawww-data1.0.0.rc ROADMAP: - Refactor to use soon-to-be-extracted axiom-types - Add ability to pass in custom reader/writer *objects* - Refactor to use extracted equalizer gem - Tune Adamantium usage to memoize common methods in Attribute virtus-2.0.0/.gitignore0000644000004100000410000000041414072243150015062 0ustar www-datawww-data## MAC OS .DS_Store ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## VIM *.swp ## Rubinius *.rbc .rbx ## PROJECT::GENERAL *.gem coverage profiling turbulence rdoc pkg tmp doc log .yardoc measurements ## BUNDLER .bundle Gemfile.lock bin/ ## PROJECT::SPECIFIC virtus-2.0.0/LICENSE0000644000004100000410000000204614072243150014102 0ustar www-datawww-dataCopyright (c) 2011-2013 Piotr Solnica Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. virtus-2.0.0/.pelusa.yml0000644000004100000410000000014014072243150015160 0ustar www-datawww-datasources: lib/**/*.rb lints: InstanceVariables: limit: 3 LineRestriction: limit: 124virtus-2.0.0/Rakefile0000644000004100000410000000041714072243150014542 0ustar www-datawww-datarequire "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task default: [:spec] begin require "rubocop/rake_task" Rake::Task[:default].enhance [:rubocop] RuboCop::RakeTask.new do |task| task.options << "--display-cop-names" end rescue LoadError end virtus-2.0.0/lib/0000755000004100000410000000000014072243150013641 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/0000755000004100000410000000000014072243150015175 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/version.rb0000644000004100000410000000005514072243150017207 0ustar www-datawww-datamodule Virtus VERSION = '2.0.0'.freeze end virtus-2.0.0/lib/virtus/value_object.rb0000644000004100000410000001002214072243150020157 0ustar www-datawww-datamodule Virtus # Include this Module for Value Object semantics # # The idea is that instances should be immutable and compared based on state # (rather than identity, as is typically the case) # # @example # class GeoLocation # include Virtus::ValueObject # attribute :latitude, Float # attribute :longitude, Float # end # # location = GeoLocation.new(:latitude => 10, :longitude => 100) # same_location = GeoLocation.new(:latitude => 10, :longitude => 100) # location == same_location #=> true # hash = { location => :foo } # hash[same_location] #=> :foo module ValueObject # Callback to configure including Class as a Value Object # # Including Class will include Virtus and have additional # value object semantics defined in this module # # @return [Undefined] # # TODO: stacking modules is getting painful # time for Facets' module_inheritance, ActiveSupport::Concern or the like # # @api private def self.included(base) Virtus.warn "Virtus::ValueObject is deprecated and will be removed in 1.0.0 #{caller.first}" base.instance_eval do include Virtus include InstanceMethods extend ClassMethods extend AllowedWriterMethods private :attributes= end end private_class_method :included module InstanceMethods # ValueObjects are immutable and can't be cloned # # They always represent the same value # # @example # # value_object.clone === value_object # => true # # @return [self] # # @api public def clone self end alias dup clone # Create a new ValueObject by combining the passed attribute hash with # the instances attributes. # # @example # # number = PhoneNumber.new(kind: "mobile", number: "123-456-78-90") # number.with(number: "987-654-32-10") # # => # # # @return [Object] # # @api public def with(attribute_updates) self.class.new(attribute_set.get(self).merge(attribute_updates)) end end module AllowedWriterMethods # The list of writer methods that can be mass-assigned to in #attributes= # # @return [Set] # # @api private def allowed_writer_methods @allowed_writer_methods ||= begin allowed_writer_methods = super allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="} allowed_writer_methods.to_set.freeze end end end module ClassMethods # Define an attribute on the receiver # # The Attribute will have private writer methods (eg., immutable instances) # and be used in equality/equivalence comparisons # # @example # class GeoLocation # include Virtus::ValueObject # # attribute :latitude, Float # attribute :longitude, Float # end # # @see Virtus::ClassMethods.attribute # # @return [self] # # @api public def attribute(name, type, options = {}) equalizer << name super name, type, options.merge(:writer => :private) end # Define and include a module that provides Value Object semantics # # Included module will have #inspect, #eql?, #== and #hash # methods whose definition is based on the _keys_ argument # # @example # virtus_class.equalizer # # @return [Equalizer] # An Equalizer module which defines #inspect, #eql?, #== and #hash # for instances of this class # # @api public def equalizer @equalizer ||= begin equalizer = Virtus::Equalizer.new(name || inspect) include equalizer equalizer end end end # module ClassMethods end # module ValueObject end # module Virtus virtus-2.0.0/lib/virtus/model.rb0000644000004100000410000000265214072243150016627 0ustar www-datawww-datamodule Virtus module Model # @api private def self.included(descendant) super descendant.send(:include, ClassInclusions) end # @api private def self.extended(descendant) super descendant.extend(Extensions) end module Core # @api private def self.included(descendant) super descendant.extend(ClassMethods) descendant.send(:include, ClassInclusions::Methods) descendant.send(:include, InstanceMethods) end private_class_method :included # @api private def self.extended(descendant) super descendant.extend(Extensions::Methods) descendant.extend(InstanceMethods) end private_class_method :extended end # Core module Constructor # @api private def self.included(descendant) super descendant.send(:include, InstanceMethods::Constructor) end private_class_method :included end # Constructor module MassAssignment # @api private def self.included(descendant) super descendant.send(:include, InstanceMethods::MassAssignment) end private_class_method :included # @api private def self.extended(descendant) super descendant.extend(InstanceMethods::MassAssignment) end private_class_method :extended end # MassAssignment end # Model end # Virtus virtus-2.0.0/lib/virtus/module_extensions.rb0000644000004100000410000000412014072243150021263 0ustar www-datawww-datamodule Virtus # Virtus module that can define attributes for later inclusion # # @private module ModuleExtensions include ConstMissingExtensions # @api private def self.extended(mod) super setup(mod) end # @api private def self.setup(mod, inclusions = [Model], attribute_definitions = []) mod.instance_variable_set('@inclusions', inclusions) existing_attributes = mod.instance_variable_get('@attribute_definitions') new_attributes = (existing_attributes || []) + attribute_definitions mod.instance_variable_set('@attribute_definitions', new_attributes) end # Define an attribute in the module # # @see Virtus::Extensions#attribute # # @return [self] # # @api private def attribute(name, type = nil, options = {}) @attribute_definitions << [name, type, options] self end private # Extend an object with Virtus methods and define attributes # # @param [Object] object # # @return [undefined] # # @api private def extended(object) super @inclusions.each { |mod| object.extend(mod) } define_attributes(object) object.set_default_attributes end # Extend a class with Virtus methods and define attributes # # @param [Object] object # # @return [undefined] # # @api private def included(object) super if Class === object @inclusions.reject do |mod| object.ancestors.include?(mod) end.each do |mod| object.send(:include, mod) end define_attributes(object) else object.extend(ModuleExtensions) ModuleExtensions.setup(object, @inclusions, @attribute_definitions) end end # Define attributes on a class or instance # # @param [Object,Class] object # # @return [undefined] # # @api private def define_attributes(object) @attribute_definitions.each do |attribute_args| object.attribute(*attribute_args) end end end # module ModuleExtensions end # module Virtus virtus-2.0.0/lib/virtus/builder/0000755000004100000410000000000014072243150016623 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/builder/hook_context.rb0000644000004100000410000000213714072243150021657 0ustar www-datawww-datamodule Virtus class Builder # Context used for building "included" and "extended" hooks # # @private class HookContext attr_reader :builder, :config, :attribute_method # @api private def initialize(builder, config) @builder, @config = builder, config initialize_attribute_method end # @api private def modules modules = builder.extensions modules << Model::Constructor if constructor? modules << Model::MassAssignment if mass_assignment? modules end # @api private def constructor? config.constructor end # @api private def mass_assignment? config.mass_assignment end # @api private def finalize? config.finalize end # @api private def initialize_attribute_method method_options = builder.options @attribute_method = lambda do |name, type = nil, options = {}| super(name, type, method_options.merge(options)) end end end # HookContext end # Builder end # Virtus virtus-2.0.0/lib/virtus/attribute.rb0000644000004100000410000001327314072243150017533 0ustar www-datawww-datamodule Virtus # Attribute objects handle coercion and provide interface to hook into an # attribute set instance that's included into a class or object # # @example # # # non-strict mode # attr = Virtus::Attribute.build(Integer) # attr.coerce('1') # # => 1 # # # strict mode # attr = Virtus::Attribute.build(Integer, :strict => true) # attr.coerce('not really coercible') # # => Virtus::CoercionError: Failed to coerce "not really coercible" into Integer # class Attribute extend DescendantsTracker, Options, TypeLookup include Equalizer.new(inspect) << :type << :options accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize, :nullify_blank strict false required true accessor :public finalize true nullify_blank false # @see Virtus.coerce # # @deprecated # # @api public def self.coerce(value = Undefined) Virtus.warn "#{self}.coerce is deprecated and will be removed in 1.0.0. Use Virtus.coerce instead: ##{caller.first}" return Virtus.coerce if value.equal?(Undefined) Virtus.coerce = value self end # Return type of this attribute # # @return [Axiom::Types::Type] # # @api public attr_reader :type # @api private attr_reader :primitive, :options, :default_value, :coercer # Builds an attribute instance # # @param [Class,Array,Hash,String,Symbol] type # this can be an explicit class or an object from which virtus can infer # the type # # @param [#to_hash] options # optional extra options hash # # @return [Attribute] # # @api public def self.build(type, options = {}) Builder.call(type, options) end # @api private def self.build_coercer(type, options = {}) Coercer.new(type, options.fetch(:configured_coercer) { Virtus.coercer }) end # @api private def self.build_type(definition) Axiom::Types.infer(definition.primitive) end # @api private def self.merge_options!(*) # noop end # @api private def initialize(type, options) @type = type @primitive = type.primitive @options = options @default_value = options.fetch(:default_value) @coercer = options.fetch(:coercer) end # Coerce the input into the expected type # # @example # # attr = Virtus::Attribute.build(String) # attr.coerce(:one) # => 'one' # # @param [Object] input # # @api public def coerce(input) coercer.call(input) end # Return a new attribute with the new name # # @param [Symbol] name # # @return [Attribute] # # @api public def rename(name) self.class.build(type, options.merge(:name => name)) end # Return if the given value was coerced # # @param [Object] value # # @return [Boolean] # # @api public def value_coerced?(value) coercer.success?(primitive, value) end # Return if the attribute is coercible # # @example # # attr = Virtus::Attribute.build(String, :coerce => true) # attr.coercible? # => true # # attr = Virtus::Attribute.build(String, :coerce => false) # attr.coercible? # => false # # @return [Boolean] # # @api public def coercible? kind_of?(Coercible) end # Return if the attribute has lazy default value evaluation # # @example # # attr = Virtus::Attribute.build(String, :lazy => true) # attr.lazy? # => true # # attr = Virtus::Attribute.build(String, :lazy => false) # attr.lazy? # => false # # @return [Boolean] # # @api public def lazy? kind_of?(LazyDefault) end # Return if the attribute is in the strict coercion mode # # @example # # attr = Virtus::Attribute.build(String, :strict => true) # attr.strict? # => true # # attr = Virtus::Attribute.build(String, :strict => false) # attr.strict? # => false # # @return [Boolean] # # @api public def strict? kind_of?(Strict) end # Return if the attribute is in the nullify blank coercion mode # # @example # # attr = Virtus::Attribute.build(String, :nullify_blank => true) # attr.nullify_blank? # => true # # attr = Virtus::Attribute.build(String, :nullify_blank => false) # attr.nullify_blank? # => false # # @return [Boolean] # # @api public def nullify_blank? kind_of?(NullifyBlank) end # Return if the attribute is accepts nil values as valid coercion output # # @example # # attr = Virtus::Attribute.build(String, :required => true) # attr.required? # => true # # attr = Virtus::Attribute.build(String, :required => false) # attr.required? # => false # # @return [Boolean] # # @api public def required? options[:required] end # Return if the attribute was already finalized # # @example # # attr = Virtus::Attribute.build(String, :finalize => true) # attr.finalized? # => true # # attr = Virtus::Attribute.build(String, :finalize => false) # attr.finalized? # => false # # @return [Boolean] # # @api public def finalized? frozen? end # @api private def define_accessor_methods(attribute_set) attribute_set.define_reader_method(self, name, options[:reader]) attribute_set.define_writer_method(self, "#{name}=", options[:writer]) end # @api private def finalize freeze self end end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/0000755000004100000410000000000014072243150017200 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/attribute/accessor.rb0000644000004100000410000000463614072243150021340 0ustar www-datawww-datamodule Virtus class Attribute # Accessor extension provides methods to read and write attributes # # @example # # attribute = Virtus::Attribute.build(String, :name => :email) # model = Class.new { attr_reader :email } # object = model.new # # attribute.set(object, 'jane@doe.com') # attribute.get(object) # => 'jane@doe.com' # module Accessor # Return name of this accessor attribute # # @return [Symbol] # # @api public attr_reader :name # Return instance_variable_name used by this accessor # # @api private attr_reader :instance_variable_name # @api private def self.extended(descendant) super name = descendant.options.fetch(:name).to_sym descendant.instance_variable_set('@name', name) descendant.instance_variable_set('@instance_variable_name', "@#{name}") end # Return if attribute value is defined # # @param [Object] instance # # @return [Boolean] # # @api public def defined?(instance) instance.instance_variable_defined?(instance_variable_name) end # Return value of the attribute # # @param [Object] instance # # @return [Object] # # @api public def get(instance) instance.instance_variable_get(instance_variable_name) end # Set value of the attribute # # @param [Object] instance # @param [Object] value # # @return [Object] value that was set # # @api public def set(instance, value) instance.instance_variable_set(instance_variable_name, value) end # Set default value # # @param [Object] instance # # @return [Object] value that was set # # @api public def set_default_value(instance) set(instance, default_value.call(instance, self)) end # Returns a Boolean indicating whether the reader method is public # # @return [Boolean] # # @api private def public_reader? options[:reader] == :public end # Returns a Boolean indicating whether the writer method is public # # @return [Boolean] # # @api private def public_writer? options[:writer] == :public end end # Accessor end # Attribute end # Virtus virtus-2.0.0/lib/virtus/attribute/nullify_blank.rb0000644000004100000410000000065614072243150022365 0ustar www-datawww-datamodule Virtus class Attribute # Attribute extension which nullifies blank attributes when coercion failed # module NullifyBlank # @see [Attribute#coerce] # # @api public def coerce(input) output = super if !value_coerced?(output) && input.to_s.empty? nil else output end end end # NullifyBlank end # Attribute end # Virtus virtus-2.0.0/lib/virtus/attribute/lazy_default.rb0000644000004100000410000000047414072243150022215 0ustar www-datawww-datamodule Virtus class Attribute module LazyDefault # @api public def get(instance) if instance.instance_variable_defined?(instance_variable_name) super else set_default_value(instance) end end end # LazyDefault end # Attribute end # Virtus virtus-2.0.0/lib/virtus/attribute/default_value/0000755000004100000410000000000014072243150022020 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/attribute/default_value/from_callable.rb0000644000004100000410000000141114072243150025124 0ustar www-datawww-datamodule Virtus class Attribute class DefaultValue # Represents default value evaluated via a callable object # # @api private class FromCallable < DefaultValue # Return if the class can handle the value # # @param [Object] value # # @return [Boolean] # # @api private def self.handle?(value) value.respond_to?(:call) end # Evaluates the value via value#call # # @param [Object] args # # @return [Object] evaluated value # # @api private def call(*args) @value.call(*args) end end # class FromCallable end # class DefaultValue end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/default_value/from_symbol.rb0000644000004100000410000000155414072243150024702 0ustar www-datawww-datamodule Virtus class Attribute class DefaultValue # Represents default value evaluated via a symbol # # @api private class FromSymbol < DefaultValue # Return if the class can handle the value # # @param [Object] value # # @return [Boolean] # # @api private def self.handle?(value) value.is_a?(Symbol) end # Evaluates the value via instance#public_send(value) # # Symbol value is returned if the instance doesn't respond to value # # @return [Object] evaluated value # # @api private def call(instance, _) instance.respond_to?(@value, true) ? instance.send(@value) : @value end end # class FromSymbol end # class DefaultValue end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/default_value/from_clonable.rb0000644000004100000410000000154514072243150025154 0ustar www-datawww-datamodule Virtus class Attribute class DefaultValue # Represents default value evaluated via a clonable object # # @api private class FromClonable < DefaultValue SINGLETON_CLASSES = [ ::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol ].freeze # Return if the class can handle the value # # @param [Object] value # # @return [Boolean] # # @api private def self.handle?(value) SINGLETON_CLASSES.none? { |klass| value.kind_of?(klass) } end # Evaluates the value via value#clone # # @return [Object] evaluated value # # @api private def call(*) @value.clone end end # class FromClonable end # class DefaultValue end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/hash.rb0000644000004100000410000000651014072243150020452 0ustar www-datawww-datamodule Virtus class Attribute # Handles attributes with Hash type # class Hash < Attribute primitive ::Hash default primitive.new # @api private attr_reader :key_type, :value_type # FIXME: remove this once axiom-types supports it # # @private Type = Struct.new(:key_type, :value_type) do def self.infer(type) if axiom_type?(type) new(type.key_type, type.value_type) else type_options = infer_key_and_value_types(type) key_class = determine_type(type_options.fetch(:key_type, Object)) value_class = determine_type(type_options.fetch(:value_type, Object)) new(key_class, value_class) end end # @api private def self.pending?(primitive) primitive.is_a?(String) || primitive.is_a?(Symbol) end # @api private def self.axiom_type?(type) type.is_a?(Class) && type < Axiom::Types::Type end # @api private def self.determine_type(type) return type if pending?(type) if EmbeddedValue.handles?(type) type else Axiom::Types.infer(type) end end # @api private def self.infer_key_and_value_types(type) return {} unless type.kind_of?(::Hash) if type.size > 1 raise ArgumentError, "more than one [key => value] pair in `#{type}`" else key_type, value_type = type.keys.first, type.values.first key_primitive = if key_type.is_a?(Class) && key_type < Attribute && key_type.primitive key_type.primitive else key_type end value_primitive = if value_type.is_a?(Class) && value_type < Attribute && value_type.primitive value_type.primitive else value_type end { :key_type => key_primitive, :value_type => value_primitive} end end # @api private def coercion_method :to_hash end # @api private def primitive ::Hash end end # @api private def self.build_type(definition) Type.infer(definition.type) end # @api private def self.merge_options!(type, options) options[:key_type] ||= Attribute.build(type.key_type, :strict => options[:strict]) options[:value_type] ||= Attribute.build(type.value_type, :strict => options[:strict]) end # Coerce members # # @see [Attribute#coerce] # # @api public def coerce(*) coerced = super return coerced unless coerced.respond_to?(:each_with_object) coerced.each_with_object({}) do |(key, value), hash| hash[key_type.coerce(key)] = value_type.coerce(value) end end # @api private def finalize return self if finalized? @key_type = options[:key_type].finalize @value_type = options[:value_type].finalize super end # @api private def finalized? super && key_type.finalized? && value_type.finalized? end end # class Hash end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/default_value.rb0000644000004100000410000000202514072243150022344 0ustar www-datawww-datamodule Virtus class Attribute # Class representing the default value option # # @api private class DefaultValue extend DescendantsTracker include Equalizer.new(inspect) << :value # Builds a default value instance # # @return [Virtus::Attribute::DefaultValue] # # @api private def self.build(*args) klass = descendants.detect { |descendant| descendant.handle?(*args) } || self klass.new(*args) end # Returns the value instance # # @return [Object] # # @api private attr_reader :value # Initializes an default value instance # # @param [Object] value # # @return [undefined] # # @api private def initialize(value) @value = value end # Evaluates the value # # @return [Object] evaluated value # # @api private def call(*) value end end # class DefaultValue end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/coercer.rb0000644000004100000410000000165214072243150021153 0ustar www-datawww-datamodule Virtus class Attribute # Coercer accessor wrapper # # @api private class Coercer < Virtus::Coercer # @api private attr_reader :method, :coercers # Initialize a new coercer object # # @param [Object] coercers accessor # @param [Symbol] coercion method # # @return [undefined] # # @api private def initialize(type, coercers) super(type) @method = type.coercion_method @coercers = coercers end # Coerce given value # # @return [Object] # # @api private def call(value) coercers[value.class].public_send(method, value) rescue ::Coercible::UnsupportedCoercion value end # @api public def success?(primitive, value) coercers[primitive].coerced?(value) end end # class Coercer end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/boolean.rb0000644000004100000410000000257614072243150021156 0ustar www-datawww-datamodule Virtus class Attribute # Boolean attribute allows true or false values to be set # Additionally it adds boolean reader method, like "admin?" # # @example # class Post # include Virtus # # attribute :published, Boolean # end # # post = Post.new(:published => false) # post.published? # => false # class Boolean < Attribute primitive TrueClass # @api private def self.build_type(*) Axiom::Types::Boolean end # Returns if the given value is either true or false # # @example # boolean = Virtus::Attribute::Boolean.new(:bool) # boolean.value_coerced?(true) # => true # boolean.value_coerced?(false) # => true # boolean.value_coerced?(1) # => false # boolean.value_coerced?('true') # => false # # @return [Boolean] # # @api public def value_coerced?(value) value.equal?(true) || value.equal?(false) end # Creates an attribute reader method as a query # # @param [Module] mod # # @return [undefined] # # @api private def define_accessor_methods(attribute_set) super attribute_set.define_reader_method(self, "#{name}?", options[:reader]) end end # class Boolean end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/coercible.rb0000644000004100000410000000056114072243150021456 0ustar www-datawww-datamodule Virtus class Attribute # Attribute extension providing coercion when setting an attribute value # module Coercible # Coerce value before setting # # @see Accessor#set # # @api public def set(instance, value) super(instance, coerce(value)) end end # Coercible end # Attribute end # Virtus virtus-2.0.0/lib/virtus/attribute/builder.rb0000644000004100000410000001065414072243150021161 0ustar www-datawww-datamodule Virtus # Attribute placeholder used when type constant is passed as a string or symbol # # @private class PendingAttribute attr_reader :type, :options, :name # @api private def initialize(type, options) @type, @options = type.to_s, options @name = options[:name] end # @api private def finalize Attribute::Builder.call(determine_type, options).finalize end # @api private def finalized? false end # @api private def determine_type if type.include?('::') Virtus.constantize(type) else Object.const_get(type) end end end # PendingAttribute # Extracts the actual type primitive from input type # # @private class TypeDefinition attr_reader :type, :primitive # @api private def initialize(type) @type = type initialize_primitive end # @api private def pending? @pending if defined?(@pending) end private # @api private def initialize_primitive @primitive = if type.instance_of?(String) || type.instance_of?(Symbol) if !type.to_s.include?('::') && Object.const_defined?(type) Object.const_get(type) elsif not Attribute::Builder.determine_type(type) @pending = true type else type end elsif not type.is_a?(Class) type.class else type end end end class Attribute # Builder is used to set up an attribute instance based on input type and options # # @private class Builder attr_reader :attribute, :options, :type_definition, :klass, :type # @api private def self.call(type, options = {}) type_definition = TypeDefinition.new(type) if type_definition.pending? PendingAttribute.new(type, options) else new(type_definition, options).attribute end end # @api private def self.determine_type(klass, default = nil) type = Attribute.determine_type(klass) if klass.is_a?(Class) type ||= if klass < Axiom::Types::Type determine_type(klass.primitive) elsif EmbeddedValue.handles?(klass) EmbeddedValue elsif klass < Enumerable && !(klass <= Range) Collection end end type || default end # @api private def initialize(type_definition, options) @type_definition = type_definition initialize_class initialize_type initialize_options(options) initialize_default_value initialize_coercer initialize_attribute end private # @api private def initialize_class @klass = self.class.determine_type(type_definition.primitive, Attribute) end # @api private def initialize_type @type = klass.build_type(type_definition) end # @api private def initialize_options(options) @options = klass.options.merge(:coerce => Virtus.coerce).update(options) klass.merge_options!(type, @options) determine_visibility end # @api private def initialize_default_value options.update(:default_value => DefaultValue.build(options[:default])) end # @api private def initialize_coercer options.update(:coercer => determine_coercer) end # @api private def initialize_attribute @attribute = klass.new(type, options) @attribute.extend(Accessor) if options[:name] @attribute.extend(Coercible) if options[:coerce] @attribute.extend(NullifyBlank) if options[:nullify_blank] @attribute.extend(Strict) if options[:strict] @attribute.extend(LazyDefault) if options[:lazy] @attribute.finalize if options[:finalize] end # @api private def determine_coercer options.fetch(:coercer) { klass.build_coercer(type, options) } end # @api private def determine_visibility default_accessor = options.fetch(:accessor) reader_visibility = options.fetch(:reader, default_accessor) writer_visibility = options.fetch(:writer, default_accessor) options.update(:reader => reader_visibility, :writer => writer_visibility) end end # class Builder end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/strict.rb0000644000004100000410000000077514072243150021046 0ustar www-datawww-datamodule Virtus class Attribute # Attribute extension which raises CoercionError when coercion failed # module Strict # @see [Attribute#coerce] # # @raises [CoercionError] when coercer failed # # @api public def coerce(*) output = super if value_coerced?(output) || !required? && output.nil? output else raise CoercionError.new(output, self) end end end # Strict end # Attribute end # Virtus virtus-2.0.0/lib/virtus/attribute/collection.rb0000644000004100000410000000522514072243150021664 0ustar www-datawww-datamodule Virtus class Attribute # Collection attribute handles enumerable-like types # # Handles coercing members to the designated member type. # class Collection < Attribute default Proc.new { |_, attribute| attribute.primitive.new } # @api private attr_reader :member_type # FIXME: temporary hack, remove when Axiom::Type works with EV as member_type Type = Struct.new(:primitive, :member_type) do def self.infer(type, primitive) return type if axiom_type?(type) klass = Axiom::Types.infer(type) member = infer_member_type(type) || Object if EmbeddedValue.handles?(member) || pending?(member) Type.new(primitive, member) else klass.new { primitive primitive member_type Axiom::Types.infer(member) } end end def self.pending?(primitive) primitive.is_a?(String) || primitive.is_a?(Symbol) end def self.axiom_type?(type) type.is_a?(Class) && type < Axiom::Types::Type end def self.infer_member_type(type) return unless type.respond_to?(:count) member_type = if type.count > 1 raise NotImplementedError, "build SumType from list of types (#{type})" else type.first end if member_type.is_a?(Class) && member_type < Attribute && member_type.primitive member_type.primitive else member_type end end def coercion_method :to_array end end # @api private def self.build_type(definition) Type.infer(definition.type, definition.primitive) end # @api private def self.merge_options!(type, options) options[:member_type] ||= Attribute.build(type.member_type, strict: options[:strict]) end # @api public def coerce(value) coerced = super return coerced unless coerced.respond_to?(:each_with_object) coerced.each_with_object(primitive.new) do |entry, collection| collection << member_type.coerce(entry) end end # @api public def value_coerced?(value) super && value.all? { |item| member_type.value_coerced? item } end # @api private def finalize return self if finalized? @member_type = @options[:member_type].finalize super end # @api private def finalized? super && member_type.finalized? end end # class Collection end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/attribute/embedded_value.rb0000644000004100000410000000321414072243150022452 0ustar www-datawww-datamodule Virtus class Attribute # EmbeddedValue handles virtus-like objects, OpenStruct and Struct # class EmbeddedValue < Attribute TYPES = [Struct, OpenStruct, Virtus, Model::Constructor].freeze # Builds Struct-like instance with attributes passed to the constructor as # a list of args rather than a hash # # @private class FromStruct < Virtus::Coercer # @api public def call(input) if input.kind_of?(primitive) input elsif not input.nil? primitive.new(*input) end end end # FromStruct # Builds OpenStruct-like instance with attributes passed to the constructor # as a hash # # @private class FromOpenStruct < Virtus::Coercer # @api public def call(input) if input.kind_of?(primitive) input elsif not input.nil? primitive.new(input) end end end # FromOpenStruct # @api private def self.handles?(klass) klass.is_a?(Class) && TYPES.any? { |type| klass <= type } end # @api private def self.build_type(definition) Axiom::Types::Object.new { primitive definition.primitive } end # @api private def self.build_coercer(type, _options) primitive = type.primitive if primitive < Virtus || primitive < Model::Constructor || primitive <= OpenStruct FromOpenStruct.new(type) elsif primitive < Struct FromStruct.new(type) end end end # class EmbeddedValue end # class Attribute end # module Virtus virtus-2.0.0/lib/virtus/instance_methods.rb0000644000004100000410000001135114072243150021052 0ustar www-datawww-datamodule Virtus # Instance methods that are added when you include Virtus module InstanceMethods module Constructor # Set attributes during initialization of an object # # @param [#to_hash] attributes # the attributes hash to be set # # @return [undefined] # # @api private def initialize(attributes = nil) attribute_set.set(self, attributes) if attributes set_default_attributes end end # Constructor module MassAssignment # Returns a hash of all publicly accessible attributes # # @example # class User # include Virtus # # attribute :name, String # attribute :age, Integer # end # # user = User.new(:name => 'John', :age => 28) # user.attributes # => { :name => 'John', :age => 28 } # # @return [Hash] # # @api public def attributes attribute_set.get(self) end alias_method :to_hash, :attributes alias_method :to_h, :attributes # Mass-assign attribute values # # Keys in the +attributes+ param can be symbols or strings. # All referenced Attribute writer methods *will* be called. # Non-attribute setter methods on the receiver *will* be called. # # @example # class User # include Virtus # # attribute :name, String # attribute :age, Integer # end # # user = User.new # user.attributes = { :name => 'John', 'age' => 28 } # # @param [#to_hash] attributes # a hash of attribute names and values to set on the receiver # # @return [Hash] # # @api public def attributes=(attributes) attribute_set.set(self, attributes) end end # MassAssignment # Returns a value of the attribute with the given name # # @example # class User # include Virtus # # attribute :name, String # end # # user = User.new(:name => 'John') # user[:name] # => "John" # # @param [Symbol] name # a name of an attribute # # @return [Object] # a value of an attribute # # @api public def [](name) public_send(name) end # Sets a value of the attribute with the given name # # @example # class User # include Virtus # # attribute :name, String # end # # user = User.new # user[:name] = "John" # => "John" # user.name # => "John" # # @param [Symbol] name # a name of an attribute # # @param [Object] value # a value to be set # # @return [Object] # the value set on an object # # @api public def []=(name, value) public_send("#{name}=", value) end # Freeze object # # @return [self] # # @api public # # @example # # class User # include Virtus # # attribute :name, String # attribute :age, Integer # end # # user = User.new(:name => 'John', :age => 28) # user.frozen? # => false # user.freeze # user.frozen? # => true # # @api public def freeze set_default_attributes! super end # Reset an attribute to its default # # @return [self] # # @api public # # @example # # class User # include Virtus # # attribute :age, Integer, default: 21 # end # # user = User.new(:name => 'John', :age => 28) # user.age = 30 # user.age # => 30 # user.reset_attribute(:age) # user.age # => 21 # # @api public def reset_attribute(attribute_name) attribute = attribute_set[attribute_name] attribute.set_default_value(self) if attribute self end # Set default attributes # # @return [self] # # @api private def set_default_attributes attribute_set.set_defaults(self) self end # Set default attributes even lazy ones # # @return [self] # # @api public def set_default_attributes! attribute_set.set_defaults(self, proc { |object, attribute| attribute.defined?(object) }) self end private # The list of allowed public methods # # @return [Array] # # @api private def allowed_methods public_methods.map(&:to_s) end # @api private def assert_valid_name(name) if respond_to?(:attributes) && name.to_sym == :attributes || name.to_sym == :attribute_set raise ArgumentError, "#{name.inspect} is not allowed as an attribute name" end end end # module InstanceMethods end # module Virtus virtus-2.0.0/lib/virtus/class_methods.rb0000644000004100000410000000430314072243150020352 0ustar www-datawww-datamodule Virtus # Class methods that are added when you include Virtus module ClassMethods include Extensions::Methods include ConstMissingExtensions # Hook called when module is extended # # @param [Class] descendant # # @return [undefined] # # @api private def self.extended(descendant) super descendant.send(:include, AttributeSet.create(descendant)) end private_class_method :extended # Returns all the attributes defined on a Class # # @example # class User # include Virtus # # attribute :name, String # attribute :age, Integer # end # # User.attribute_set # => # # TODO: implement inspect so the output is not cluttered - solnic # # @return [AttributeSet] # # @api public def attribute_set @attribute_set end # @see Virtus::ClassMethods.attribute_set # # @deprecated # # @api public def attributes warn "#{self}.attributes is deprecated. Use #{self}.attribute_set instead: #{caller.first}" attribute_set end private # Setup descendants' own Attribute-accessor-method-hosting modules # # Descendants inherit Attribute accessor methods via Ruby's inheritance # mechanism: Attribute accessor methods are defined in a module included # in a superclass. Attributes defined on descendants add methods to the # descendant's Attributes accessor module, leaving the superclass's method # table unaffected. # # @param [Class] descendant # # @return [undefined] # # @api private def inherited(descendant) super AttributeSet.create(descendant) descendant.module_eval { include attribute_set } end # The list of allowed public methods # # @return [Array] # # @api private def allowed_methods public_instance_methods.map(&:to_s) end # @api private def assert_valid_name(name) if instance_methods.include?(:attributes) && name.to_sym == :attributes raise ArgumentError, "#{name.inspect} is not allowed as an attribute name" end end end # module ClassMethods end # module Virtus virtus-2.0.0/lib/virtus/configuration.rb0000644000004100000410000000377414072243150020404 0ustar www-datawww-datamodule Virtus # A Configuration instance class Configuration # Access the finalize setting for this instance attr_accessor :finalize # Access the coerce setting for this instance attr_accessor :coerce # Access the strict setting for this instance attr_accessor :strict # Access the nullify_blank setting for this instance attr_accessor :nullify_blank # Access the required setting for this instance attr_accessor :required # Access the constructor setting for this instance attr_accessor :constructor # Access the mass-assignment setting for this instance attr_accessor :mass_assignment # Initialized a configuration instance # # @return [undefined] # # @api private def initialize(options={}) @finalize = options.fetch(:finalize, true) @coerce = options.fetch(:coerce, true) @strict = options.fetch(:strict, false) @nullify_blank = options.fetch(:nullify_blank, false) @required = options.fetch(:required, true) @constructor = options.fetch(:constructor, true) @mass_assignment = options.fetch(:mass_assignment, true) @coercer = Coercible::Coercer.new yield self if block_given? end # Access the coercer for this instance and optional configure a # new coercer with the passed block # # @example # configuration.coercer do |config| # config.string.boolean_map = { true => '1', false => '0' } # end # # @return [Coercer] # # @api private def coercer(&block) @coercer = Coercible::Coercer.new(&block) if block_given? @coercer end # @api private def to_h { :coerce => coerce, :finalize => finalize, :strict => strict, :nullify_blank => nullify_blank, :required => required, :configured_coercer => coercer }.freeze end end # class Configuration end # module Virtus virtus-2.0.0/lib/virtus/coercer.rb0000644000004100000410000000144514072243150017150 0ustar www-datawww-datamodule Virtus # Abstract coercer class # class Coercer include Equalizer.new(inspect) << :primitive << :type # @api private attr_reader :primitive, :type # @api private def initialize(type) @type = type @primitive = type.primitive end # Coerce input value into expected primitive type # # @param [Object] input # # @return [Object] coerced input # # @api public def call(input) NotImplementedError.new("#{self.class}#call must be implemented") end # Return if the input value was successfuly coerced # # @param [Object] input # # @return [Object] coerced input # # @api public def success?(primitive, input) input.kind_of?(primitive) end end # Coercer end # Virtus virtus-2.0.0/lib/virtus/const_missing_extensions.rb0000644000004100000410000000061014072243150022655 0ustar www-datawww-datamodule Virtus module ConstMissingExtensions # Hooks into const missing process to determine types of attributes # # @param [String] name # # @return [Class] # # @api private def const_missing(name) Attribute::Builder.determine_type(name) or Axiom::Types.const_defined?(name) && Axiom::Types.const_get(name) or super end end end virtus-2.0.0/lib/virtus/extensions.rb0000644000004100000410000000527614072243150017733 0ustar www-datawww-datamodule Virtus # Extensions common for both classes and instances module Extensions WRITER_METHOD_REGEXP = /=\z/.freeze INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze RESERVED_NAMES = [:attributes].to_set.freeze # A hook called when an object is extended with Virtus # # @param [Object] object # # @return [undefined] # # @api private def self.extended(object) super object.instance_eval do extend Methods extend InstanceMethods extend InstanceMethods::MassAssignment end end private_class_method :extended module Methods # @api private def self.extended(descendant) super descendant.extend(AttributeSet.create(descendant)) end private_class_method :extended # Defines an attribute on an object's class or instance # # @example # class Book # include Virtus.model # # attribute :title, String # attribute :author, String # attribute :published_at, DateTime # attribute :page_count, Integer # attribute :index # defaults to Object # end # # @param [Symbol] name # the name of an attribute # # @param [Class,Array,Hash,Axiom::Types::Type,String,Symbol] type # the type class of an attribute # # @param [#to_hash] options # the extra options hash # # @return [self] # # @see Attribute.build # # @api public def attribute(name, type = nil, options = {}) assert_valid_name(name) attribute_set << Attribute.build(type, options.merge(:name => name)) self end # @see Virtus.default_value # # @api public def values(&block) private :attributes= if instance_methods.include?(:attributes=) yield include(Equalizer.new(name, attribute_set.map(&:name))) end # The list of writer methods that can be mass-assigned to in #attributes= # # @return [Set] # # @api private def allowed_writer_methods @allowed_writer_methods ||= begin allowed_writer_methods = allowed_methods.grep(WRITER_METHOD_REGEXP).to_set allowed_writer_methods -= INVALID_WRITER_METHODS allowed_writer_methods.freeze end end private # Return an attribute set for that instance # # @return [AttributeSet] # # @api private def attribute_set @attribute_set end end # Methods end # module Extensions end # module Virtus virtus-2.0.0/lib/virtus/class_inclusions.rb0000644000004100000410000000206614072243150021101 0ustar www-datawww-datamodule Virtus # Class-level extensions module ClassInclusions # Extends a descendant with class and instance methods # # @param [Class] descendant # # @return [undefined] # # @api private def self.included(descendant) super descendant.extend(ClassMethods) descendant.class_eval { include Methods } descendant.class_eval { include InstanceMethods } descendant.class_eval { include InstanceMethods::Constructor } descendant.class_eval { include InstanceMethods::MassAssignment } end private_class_method :included module Methods # Return a list of allowed writer method names # # @return [Set] # # @api private def allowed_writer_methods self.class.allowed_writer_methods end private # Return class' attribute set # # @return [Virtus::AttributeSet] # # @api private def attribute_set self.class.attribute_set end end # Methods end # module ClassInclusions end # module Virtus virtus-2.0.0/lib/virtus/attribute_set.rb0000644000004100000410000001266414072243150020411 0ustar www-datawww-datamodule Virtus # A set of Attribute objects class AttributeSet < Module include Enumerable # @api private def self.create(descendant) if descendant.respond_to?(:superclass) && descendant.superclass.respond_to?(:attribute_set) parent = descendant.superclass.public_send(:attribute_set) end descendant.instance_variable_set('@attribute_set', AttributeSet.new(parent)) end # Initialize an AttributeSet # # @param [AttributeSet] parent # @param [Array] attributes # # @return [undefined] # # @api private def initialize(parent = nil, attributes = []) @parent = parent @attributes = attributes.dup @index = {} reset end # Iterate over each attribute in the set # # @example # attribute_set = AttributeSet.new(attributes, parent) # attribute_set.each { |attribute| ... } # # @yield [attribute] # # @yieldparam [Attribute] attribute # each attribute in the set # # @return [self] # # @api public def each return to_enum unless block_given? @index.each { |name, attribute| yield attribute if name.kind_of?(Symbol) } self end # Adds the attributes to the set # # @example # attribute_set.merge(attributes) # # @param [Array] attributes # # @return [self] # # @api public def merge(attributes) attributes.each { |attribute| self << attribute } self end # Adds an attribute to the set # # @example # attribute_set << attribute # # @param [Attribute] attribute # # @return [self] # # @api public def <<(attribute) self[attribute.name] = attribute attribute.define_accessor_methods(self) if attribute.finalized? self end # Get an attribute by name # # @example # attribute_set[:name] # => Attribute object # # @param [Symbol] name # # @return [Attribute] # # @api public def [](name) @index[name] end # Set an attribute by name # # @example # attribute_set[:name] = attribute # # @param [Symbol] name # @param [Attribute] attribute # # @return [Attribute] # # @api public def []=(name, attribute) @attributes << attribute update_index(name, attribute) end # Reset the index when the parent is updated # # @return [self] # # @api private def reset merge_attributes(@parent) if @parent merge_attributes(@attributes) self end # Defines an attribute reader method # # @param [Attribute] attribute # @param [Symbol] method_name # @param [Symbol] visibility # # @return [undefined] # # @api private def define_reader_method(attribute, method_name, visibility) define_method(method_name) { attribute.get(self) } send(visibility, method_name) end # Defines an attribute writer method # # @param [Attribute] attribute # @param [Symbol] method_name # @param [Symbol] visibility # # @return [undefined] # # @api private def define_writer_method(attribute, method_name, visibility) define_method(method_name) { |value| attribute.set(self, value) } send(visibility, method_name) end # Get values of all attributes defined for this class, ignoring privacy # # @return [Hash] # # @api private def get(object) each_with_object({}) do |attribute, attributes| name = attribute.name attributes[name] = object.__send__(name) if attribute.public_reader? end end # Mass-assign attribute values # # @see Virtus::InstanceMethods#attributes= # # @return [Hash] # # @api private def set(object, attributes) coerce(attributes).each do |name, value| writer_name = "#{name}=" if object.allowed_writer_methods.include?(writer_name) object.__send__(writer_name, value) end end end # Set default attributes # # @return [self] # # @api private def set_defaults(object, filter = method(:skip_default?)) each do |attribute| next if filter.call(object, attribute) attribute.set_default_value(object) end end # Coerce attributes received to a hash # # @return [Hash] # # @api private def coerce(attributes) ::Hash.try_convert(attributes) or raise( NoMethodError, "Expected #{attributes.inspect} to respond to #to_hash" ) end # @api private def finalize each do |attribute| self << attribute.finalize unless attribute.finalized? end end private # @api private def skip_default?(object, attribute) attribute.lazy? || attribute.defined?(object) end # Merge the attributes into the index # # @param [Array] attributes # # @return [undefined] # # @api private def merge_attributes(attributes) attributes.each { |attribute| update_index(attribute.name, attribute) } end # Update the symbol and string indexes with the attribute # # @param [Symbol] name # # @param [Attribute] attribute # # @return [undefined] # # @api private def update_index(name, attribute) @index[name] = @index[name.to_s.freeze] = attribute end end # class AttributeSet end # module Virtus virtus-2.0.0/lib/virtus/builder.rb0000644000004100000410000000551214072243150017153 0ustar www-datawww-datamodule Virtus # Class to build a Virtus module with it's own config # # This allows for individual Virtus modules to be included in # classes and not impacted by the global Virtus config, # which is implemented using Virtus::config. # # @private class Builder # Return module # # @return [Module] # # @api private attr_reader :mod # Return config # # @return [config] # # @api private attr_reader :config # @api private def self.call(options, &block) new(Configuration.new(options, &block)).mod end # @api private def self.pending @pending ||= [] end # Initializes a new Builder # # @param [Configuration] config # @param [Module] mod # # @return [undefined] # # @api private def initialize(conf, mod = Module.new) @config, @mod = conf, mod add_included_hook add_extended_hook end # @api private def extensions [Model::Core] end # @api private def options config.to_h end private # Adds the .included hook to the anonymous module which then defines the # .attribute method to override the default. # # @return [Module] # # @api private def add_included_hook with_hook_context do |context| mod.define_singleton_method :included do |object| Builder.pending << object unless context.finalize? context.modules.each { |mod| object.send(:include, mod) } object.define_singleton_method(:attribute, context.attribute_method) end end end # @api private def add_extended_hook with_hook_context do |context| mod.define_singleton_method :extended do |object| context.modules.each { |mod| object.extend(mod) } object.define_singleton_method(:attribute, context.attribute_method) end end end # @api private def with_hook_context yield(HookContext.new(self, config)) end end # class Builder # @private class ModelBuilder < Builder end # ModelBuilder # @private class ModuleBuilder < Builder private # @api private def add_included_hook with_hook_context do |context| mod.define_singleton_method :included do |object| super(object) object.extend(ModuleExtensions) ModuleExtensions.setup(object, context.modules) object.define_singleton_method(:attribute, context.attribute_method) end end end end # ModuleBuilder # @private class ValueObjectBuilder < Builder # @api private def extensions super << ValueObject::AllowedWriterMethods << ValueObject::InstanceMethods end # @api private def options super.merge(:writer => :private) end end # ValueObjectBuilder end # module Virtus virtus-2.0.0/lib/virtus/support/0000755000004100000410000000000014072243150016711 5ustar www-datawww-datavirtus-2.0.0/lib/virtus/support/type_lookup.rb0000644000004100000410000000512414072243150021612 0ustar www-datawww-datamodule Virtus # A module that adds type lookup to a class module TypeLookup TYPE_FORMAT = /\A[A-Z]\w*\z/.freeze # Set cache ivar on the model # # @param [Class] model # # @return [undefined] # # @api private def self.extended(model) model.instance_variable_set('@type_lookup_cache', {}) end # Returns a descendant based on a name or class # # @example # MyClass.determine_type('String') # => MyClass::String # # @param [Class, #to_s] class_or_name # name of a class or a class itself # # @return [Class] # a descendant # # @return [nil] # nil if the type cannot be determined by the class_or_name # # @api public def determine_type(class_or_name) @type_lookup_cache[class_or_name] ||= determine_type_and_cache(class_or_name) end # Return the default primitive supported # # @return [Class] # # @api private def primitive raise NotImplementedError, "#{self}.primitive must be implemented" end private # @api private def determine_type_and_cache(class_or_name) case class_or_name when singleton_class determine_type_from_descendant(class_or_name) when Class determine_type_from_primitive(class_or_name) else determine_type_from_string(class_or_name.to_s) end end # Return the class given a descendant # # @param [Class] descendant # # @return [Class] # # @api private def determine_type_from_descendant(descendant) descendant if descendant < self end # Return the class given a primitive # # @param [Class] primitive # # @return [Class] # # @return [nil] # nil if the type cannot be determined by the primitive # # @api private def determine_type_from_primitive(primitive) type = nil descendants.select(&:primitive).reverse_each do |descendant| descendant_primitive = descendant.primitive next unless primitive <= descendant_primitive type = descendant if type.nil? or type.primitive > descendant_primitive end type end # Return the class given a string # # @param [String] string # # @return [Class] # # @return [nil] # nil if the type cannot be determined by the string # # @api private def determine_type_from_string(string) if string =~ TYPE_FORMAT and const_defined?(string, *EXTRA_CONST_ARGS) const_get(string, *EXTRA_CONST_ARGS) end end end # module TypeLookup end # module Virtus virtus-2.0.0/lib/virtus/support/equalizer.rb0000644000004100000410000000552714072243150021250 0ustar www-datawww-datamodule Virtus # Define equality, equivalence and inspection methods class Equalizer < Module # Initialize an Equalizer with the given keys # # Will use the keys with which it is initialized to define #cmp?, # #hash, and #inspect # # @param [String] name # # @param [Array] keys # # @return [undefined] # # @api private def initialize(name, keys = []) @name = name.dup.freeze @keys = keys.dup define_methods include_comparison_methods end # Append a key and compile the equality methods # # @return [Equalizer] self # # @api private def <<(key) @keys << key self end private # Define the equalizer methods based on #keys # # @return [undefined] # # @api private def define_methods define_cmp_method define_hash_method define_inspect_method end # Define an #cmp? method based on the instance's values identified by #keys # # @return [undefined] # # @api private def define_cmp_method keys = @keys define_method(:cmp?) do |comparator, other| keys.all? { |key| send(key).send(comparator, other.send(key)) } end end # Define a #hash method based on the instance's values identified by #keys # # @return [undefined] # # @api private def define_hash_method keys = @keys define_method(:hash) do keys.map { |key| send(key) }.push(self.class).hash end end # Define an inspect method that reports the values of the instance's keys # # @return [undefined] # # @api private def define_inspect_method name, keys = @name, @keys define_method(:inspect) do "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>" end end # Include the #eql? and #== methods # # @return [undefined] # # @api private def include_comparison_methods module_eval { include Methods } end # The comparison methods module Methods # Compare the object with other object for equality # # @example # object.eql?(other) # => true or false # # @param [Object] other # the other object to compare with # # @return [Boolean] # # @api public def eql?(other) instance_of?(other.class) && cmp?(__method__, other) end # Compare the object with other object for equivalency # # @example # object == other # => true or false # # @param [Object] other # the other object to compare with # # @return [Boolean] # # @api public def ==(other) other.kind_of?(self.class) && cmp?(__method__, other) end end # module Methods end # class Equalizer end # module Virtus virtus-2.0.0/lib/virtus/support/options.rb0000644000004100000410000000566314072243150020743 0ustar www-datawww-datamodule Virtus # A module that adds class and instance level options module Options # Returns default options hash for a given attribute class # # @example # Virtus::Attribute::String.options # # => {:primitive => String} # # @return [Hash] # a hash of default option values # # @api public def options accepted_options.each_with_object({}) do |option_name, options| option_value = send(option_name) options[option_name] = option_value unless option_value.nil? end end # Returns an array of valid options # # @example # Virtus::Attribute::String.accepted_options # # => [:primitive, :accessor, :reader, :writer] # # @return [Array] # the array of valid option names # # @api public def accepted_options @accepted_options ||= [] end # Defines which options are valid for a given attribute class # # @example # class MyAttribute < Virtus::Attribute # accept_options :foo, :bar # end # # @return [self] # # @api public def accept_options(*new_options) add_accepted_options(new_options) new_options.each { |option| define_option_method(option) } descendants.each { |descendant| descendant.add_accepted_options(new_options) } self end protected # Adds a reader/writer method for the give option name # # @return [undefined] # # @api private def define_option_method(option) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.#{option}(value = Undefined) # def self.primitive(value = Undefined) @#{option} = nil unless defined?(@#{option}) # @primitive = nil unless defined?(@primitive) return @#{option} if value.equal?(Undefined) # return @primitive if value.equal?(Undefined) @#{option} = value # @primitive = value self # self end # end RUBY end # Sets default options # # @param [#each] new_options # options to be set # # @return [self] # # @api private def set_options(new_options) new_options.each { |pair| send(*pair) } self end # Adds new options that an attribute class can accept # # @param [#to_ary] new_options # new options to be added # # @return [self] # # @api private def add_accepted_options(new_options) accepted_options.concat(new_options) self end private # Adds descendant to descendants array and inherits default options # # @param [Class] descendant # # @return [undefined] # # @api private def inherited(descendant) super descendant.add_accepted_options(accepted_options).set_options(options) end end # module Options end # module Virtus virtus-2.0.0/lib/virtus.rb0000644000004100000410000001503314072243150015524 0ustar www-datawww-datarequire 'ostruct' # Base module which adds Attribute API to your classes module Virtus # Provides args for const_get and const_defined? to make them behave # consistently across different versions of ruby EXTRA_CONST_ARGS = (RUBY_VERSION < '1.9' ? [] : [ false ]).freeze # Represents an undefined parameter used by auto-generated option methods Undefined = Object.new.freeze class CoercionError < StandardError attr_reader :output, :attribute def initialize(output, attribute) @output, @attribute = output, attribute super(build_message) end def build_message if attribute_name? "Failed to coerce attribute `#{attribute_name}' from #{output.inspect} into #{target_type}" else "Failed to coerce #{output.inspect} into #{target_type}" end end def attribute_name attribute.options[:name] end def attribute_name? attribute_name ? true : false end def target_type attribute.primitive.inspect end end # Extends base class or a module with virtus methods # # @param [Object] object # # @return [undefined] # # @deprecated # # @api private def self.included(object) super if Class === object Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.model' instead #{caller.first}") object.send(:include, ClassInclusions) else Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.module' instead #{caller.first}") object.extend(ModuleExtensions) end end private_class_method :included # Extends an object with virtus extensions # # @param [Object] object # # @return [undefined] # # @deprecated # # @api private def self.extended(object) Virtus.warn("extending with Virtus module is deprecated. Use 'extend(Virtus.model)' instead #{caller.first}") object.extend(Extensions) end private_class_method :extended # Sets the global coercer configuration # # @example # Virtus.coercer do |config| # config.string.boolean_map = { true => '1', false => '0' } # end # # @return [Coercible::Coercer] # # @api public def self.coercer(&block) configuration.coercer(&block) end # Sets the global coercion configuration value # # @param [Boolean] value # # @return [Virtus] # # @api public def self.coerce=(value) configuration.coerce = value self end # Returns the global coercion setting # # @return [Boolean] # # @api public def self.coerce configuration.coerce end # Provides access to the global Virtus configuration # # @example # Virtus.config do |config| # config.coerce = false # end # # @return [Configuration] # # @api public def self.config(&block) yield configuration if block_given? configuration end # Provides access to the Virtus module builder # see Virtus::ModuleBuilder # # @example # MyVirtusModule = Virtus.module { |mod| # mod.coerce = true # mod.string.boolean_map = { 'yup' => true, 'nope' => false } # } # # class Book # include MyVirtusModule # # attribute :published, Boolean # end # # # This could be made more succinct as well # class OtherBook # include Virtus.module { |m| m.coerce = false } # end # # @return [Module] # # @api public def self.model(options = {}, &block) ModelBuilder.call(options, &block) end # Builds a module for...modules # # @example # # module Common # include Virtus.module # # attribute :name, String # attribute :age, Integer # end # # class User # include Common # end # # class Admin # include Common # end # # @return [Module] # # @api public def self.module(options = {}, &block) ModuleBuilder.call(options, &block) end # Builds a module for value object models # # @example # # class GeoLocation # include Virtus.value_object # # values do # attribute :lat, Float # attribute :lng, Float # end # end # # @return [Module] # # @api public def self.value_object(options = {}, &block) ValueObjectBuilder.call(options, &block) end # Global configuration instance # # @ return [Configuration] # # @api private def self.configuration @configuration ||= Configuration.new end # @api private def self.constantize(type) inflector.constantize(type) end # @api private def self.inflector @inflector ||= begin require 'dry/inflector' Dry::Inflector.new rescue LoadError raise( NotImplementedError, 'Virtus needs dry-inflector gem to constantize namespaced constant names' ) end end # Finalize pending attributes # # @example # class User # include Virtus.model(:finalize => false) # # attribute :address, 'Address' # end # # class Address # include Virtus.model(:finalize => false) # # attribute :user, 'User' # end # # Virtus.finalize # this will resolve constant names # # @return [Array] array of finalized models # # @api public def self.finalize Builder.pending.each do |klass| klass.attribute_set.finalize end end # @api private def self.warn(msg) Kernel.warn(msg) end end # module Virtus require 'descendants_tracker' require 'axiom-types' require 'coercible' require 'virtus/support/equalizer' require 'virtus/support/options' require 'virtus/support/type_lookup' require 'virtus/model' require 'virtus/extensions' require 'virtus/const_missing_extensions' require 'virtus/class_inclusions' require 'virtus/module_extensions' require 'virtus/configuration' require 'virtus/builder' require 'virtus/builder/hook_context' require 'virtus/class_methods' require 'virtus/instance_methods' require 'virtus/value_object' require 'virtus/coercer' require 'virtus/attribute_set' require 'virtus/attribute/default_value' require 'virtus/attribute/default_value/from_clonable' require 'virtus/attribute/default_value/from_callable' require 'virtus/attribute/default_value/from_symbol' require 'virtus/attribute' require 'virtus/attribute/builder' require 'virtus/attribute/coercer' require 'virtus/attribute/accessor' require 'virtus/attribute/coercible' require 'virtus/attribute/strict' require 'virtus/attribute/lazy_default' require 'virtus/attribute/nullify_blank' require 'virtus/attribute/boolean' require 'virtus/attribute/collection' require 'virtus/attribute/hash' require 'virtus/attribute/embedded_value' virtus-2.0.0/CONTRIBUTING.md0000644000004100000410000000157714072243150015336 0ustar www-datawww-data# Current project status Virtus recently hit it's 1.0 release (2013-10-16). The focus now is on bug-fixes and maintenance while [@solnic][solnic] is away from the project. An experimental branch will be kept up-to date where proposed features and API changes can be made. Please direct your questions and issues to [@elskwid][elskwid], the maintainer. # Contributing to Virtus * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with Rakefile or version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Send me a pull request. Bonus points for topic branches. Author: [@solnic][solnic] Maintainer: [@elskwid][elskwid] [solnic]: https://github.com/solnic [elskwid]: https://github.com/elskwid virtus-2.0.0/virtus.gemspec0000644000004100000410000000162414072243150015777 0ustar www-datawww-data# -*- encoding: utf-8 -*- require File.expand_path('../lib/virtus/version', __FILE__) Gem::Specification.new do |gem| gem.name = "virtus" gem.version = Virtus::VERSION.dup gem.authors = [ "Piotr Solnica" ] gem.email = [ "piotr.solnica@gmail.com" ] gem.description = "Attributes on Steroids for Plain Old Ruby Objects" gem.summary = gem.description gem.homepage = "https://github.com/solnic/virtus" gem.license = 'MIT' gem.require_paths = [ "lib" ] gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- {spec}/*`.split("\n") gem.extra_rdoc_files = %w[LICENSE README.md TODO.md] gem.add_dependency('descendants_tracker', '~> 0.0', '>= 0.0.3') gem.add_dependency('coercible', '~> 1.0') gem.add_dependency('axiom-types', '~> 0.1') gem.add_development_dependency 'rake' gem.required_ruby_version = '>= 2.0' end virtus-2.0.0/Guardfile0000644000004100000410000000153714072243150014726 0ustar www-datawww-dataguard :rspec, spec_paths: 'spec/unit' do #run all specs if configuration is modified watch('Guardfile') { 'spec' } watch('Gemfile.lock') { 'spec' } watch('spec/spec_helper.rb') { 'spec' } # run all specs if supporting files files are modified watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec' } # run unit specs if associated lib code is modified watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}"] } watch(%r{\Alib/(.+)/support/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}/#{m[2]}"] } watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec' } # run a spec if it is modified watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z}) notification :tmux, :display_message => true end virtus-2.0.0/.yardopts0000644000004100000410000000003714072243150014741 0ustar www-datawww-data- README.md History.md LICENSE virtus-2.0.0/.ruby-gemset0000644000004100000410000000000714072243150015334 0ustar www-datawww-datavirtus virtus-2.0.0/Gemfile0000644000004100000410000000026414072243150014370 0ustar www-datawww-datasource 'https://rubygems.org' gemspec gem 'dry-inflector' gem 'rspec' gem 'bogus' gem 'simplecov', platform: :ruby gem "codeclimate-test-reporter", group: :test, require: false virtus-2.0.0/Changelog.md0000644000004100000410000003013014072243150015301 0ustar www-datawww-data# v2.0.0 2021-06-07 * [added] New method `Virtus::Atrribute::Collection#value_coerced?` (dslh) * [changed] inflecto was replaced with dry-inflector (solnic) * [changed] equalizer was replaced with the internal virtus/equalizer (solnic) * [changed] `Virtus::Attribute#==` was revised (see ef57af319334a1d4f3e0860acbde7c6d6f0eb8ef) (novikserg) * [fixed] Mass assignment bug fix (see #325) (novikserg) [Compare v1.0.5..v2.0.0](https://github.com/solnic/virtus/compare/v1.0.5...v2.0.0) # v1.0.5 2015-03-18 * [feature] Support for :nullify_blank option when configuring a virtus module (lucasmazza) [Compare v1.0.4..v1.0.5](https://github.com/solnic/virtus/compare/v1.0.4...v1.0.5) # v1.0.4 2015-01-03 * [feature] Support for :required option when configuring a virtus module (solnic) [Compare v1.0.3..v1.0.4](https://github.com/solnic/virtus/compare/v1.0.3...v1.0.4) # v1.0.3 2014-07-24 * [improvement] Expose attribute name in the exception when in strict mode (ntl) * [improvement] Set #to_h as an alias to #to_hash (fnando) * [fixed] Fix handling of nil in collection coercion (edgibbs) * [fixed] Fix issues with using multiple virtus modules (trptcolin) * [fixed] Fix handling of Range type (hampei) * [fixed] Fix strict mode for collection and hash types (solnic) [Compare v1.0.2..v1.0.3](https://github.com/solnic/virtus/compare/v1.0.2...v1.0.3) # v1.0.2 2013-12-03 * [improvement] Don’t override already-defined default values when freezing (amarshall) * [improvement] Improved performance of `AttributeSet#each` (Antti) * updated axiom-types dependency to ~> 0.1 (solnic) [Compare v1.0.1..v1.0.2](https://github.com/solnic/virtus/compare/v1.0.1...v1.0.2) # v1.0.1 2013-12-10 * [feature] re-introduce `ValueObject#with`, which was removed in the past (senny) * [fixed] strict mode for Boolean type (solnic) [Compare v1.0.0..v1.0.1](https://github.com/solnic/virtus/compare/v1.0.0...v1.0.1) # v1.0.0 2013-10-16 This release no longer works with Ruby 1.8.7. * [BREAKING CHANGE] Integrated with axiom-types, most of the attribute sub-classes are gone (solnic) * [feature] Configurable coercion via coercible integration (solnic) * [feature] Strict mode for coercions via `:strict` option (solnic) * [feature] Lazy-loaded default values via `:lazy` option (solnic) * [feature] Finalizing models solving circular-dependency issue (see #81) (solnic) * [feature] Ability to cherry-pick which extension should be included (solnic) * [feature] Ability to inject a custom coercer object via `:coercer` option (solnic) * [feature] Extension module builder with pre-defined configuration for attributes (elskwid & solnic) * [feature] `Virtus::Attribute` exposes a public API - you can easily build, rename and clone attribute instances and use their coercion power (solnic) * [feature] Ability to reset attributes to their default values (pewniak747) * [changed] A meaningful error will be raised if a reserved name is used as an attribute name (solnic) * [changed] Default value can be set via private and protected methods now (solnic) * [changed] New syntax for value objects (solnic) * [changed] Default values are now set in the constructor for non-lazy attributes (solnic) * [deprecated] `Virtus::Attribute.coerce` in favor of `Virtus.coerce` or a customized configured module (solnic) * [deprecated] `include Virtus` in favor of `include Virtus.model` (for classes) or `Virtus.module` (for modules) (solnic) * [deprecated] `include Virtus::ValueObject` in favor of `include Virtus.value_object` (solnic) * [deprecated] `Virtus#attributes` in favor of `Virtus#attribute_set` (solnic) * [fixed] const missing hook now works correctly in modules too (cored) * [fixed] value object with Hash type works correctly (solnic) * [fixed] issues with value-object subclasses and `#==` method (solnic) [Compare v0.5.4..v1.0.0](https://github.com/solnic/virtus/compare/v0.5.4...v1.0.0) # v0.5.4 2012-12-20 * [feature] Allow *any* enumerable to be a collection attribute (aptinio) * [feature] Add Integer.to_datetime and Float.to_datetime coercion (brutuscat) * [fixed] Fixed a regression with Hash attribute introduced by key/member coercion (shingara) * [fixed] Change leading non-significant digit type coercion to be coerced (maskact) [Compare v0.5.3..v0.5.4](https://github.com/solnic/virtus/compare/v0.5.3...v0.5.4) # v0.5.3 2012-12-13 * [feature] Added Hash member type coercion [example](https://github.com/solnic/virtus#hash-attributes-coercion) (greyblake) * [fixed] Fixed issues with String=>Integer coercion and e-notation (greyblake) * [changed] Replaced internal DescendantsTracker with the extracted gem (solnic) * [internal] Switched to rspec 2 and mutant for mutation testing (mbj) [Compare v0.5.2..v0.5.3](https://github.com/solnic/virtus/compare/v0.5.2...v0.5.3) # v0.5.2 2012-09-01 * [feature] Object is now the default attribute type (dkubb) * [fixed] Fix module inclusion problems (dkubb) * [fixed] Evaluate default values when freezing an object (mbj) * [fixed] String representation of a big integer is now properly coerced to an integer (greyblake) * [changed] AttributeSet is now a module responsible for defining attribute methods (emmanuel) [Compare v0.5.1..v0.5.2](https://github.com/solnic/virtus/compare/v0.5.1...v0.5.2) # v0.5.1 2012-06-11 * [fixed] EV properly handle nil as the value (solnic) [Compare v0.5.0..v0.5.1](https://github.com/solnic/virtus/compare/v0.5.0...v0.5.1) # v0.5.0 2012-06-08 * [feature] Support for extending objects (solnic) * [feature] Support for defining attributes in modules (solnic) * [feature] Support for Struct as an EmbeddedValue or ValueObject attribute (solnic) * [changed] Allow any input for EmbeddedValue and ValueObject constructors (solnic) * [changed] ValueObject instances cannot be duped or cloned (senny) [Compare v0.4.2..v0.5.0](https://github.com/solnic/virtus/compare/v0.4.2...v0.5.0) # v0.4.2 2012-05-08 * [updated] Bump backports dep to ~> 2.5.3 (solnic) [Compare v0.4.1..v0.4.2](https://github.com/solnic/virtus/compare/v0.4.1...v0.4.2) # v0.4.1 2012-05-06 * [changed] backports gem is now a runtime dependency (solnic) * [BREAKING CHANGE] Renamed Virtus::DefaultValue#evaluate => Virtus::DefaultValue#call (solnic) * [BREAKING CHANGE] Renamed Virtus::ValueObject::Equalizer to Virtus::Equalizer (dkubb) [Compare v0.4.0..v0.4.1](https://github.com/solnic/virtus/compare/v0.4.0...v0.4.1) # v0.4.0 2012-03-22 * [improvement] Add a caching mechanism for type lookups (solnic) * [fixed] Fix attributes mass-assignment when nil is passed (fgrehm) * [changed] Replace usage of #to_hash with Hash.try_convert (dkubb) [Compare v0.3.0..v0.4.0](https://github.com/solnic/virtus/compare/v0.3.0...v0.4.0) # v0.3.0 2012-02-25 * [feature] Support for default values from a symbol (which can be a method name) (solnic) * [feature] Support for mass-assignment via custom setters not generated with attribute (fgrehm) * [feature] Virtus::Coercion::String.to_constant handles namespaced names (dkubb) * [feature] New coercion: Virtus::Coercion::Object.to_array (dkubb) * [feature] New coercion: Virtus::Coercion::Object.to_hash (dkubb) * [feature] New coercion: Virtus::Coercion::Object.to_string (dkubb) * [feature] New coercion: Virtus::Coercion::Object.to_integer (dkubb) * [changed] EmbeddedValue relies on @primitive setting rather than @model (mbj) * [BREAKING CHANGE] Removed Attribute#writer_visibility in favor of Attribute#public_writer? (solnic) * [BREAKING CHANGE] Removed Attribute#reader_visibility in favor of Attribute#public_reader? (solnic) * [BREAKING CHANGE] Removed Attribute#instance_variable_name - this is a private ivar (solnic) * [BREAKING CHANGE] Removed Equalizer#host_name and Equalizer#keys (solnic) [Compare v0.2.0..v0.3.0](https://github.com/solnic/virtus/compare/v0.2.0...v0.3.0) # v0.2.0 2012-02-08 * [feature] Support for Value Objects (emmanuel) * [feature] New Symbol attribute (solnic) * [feature] Time => Integer coercion (solnic) [Compare v0.1.0..v0.2.0](https://github.com/solnic/virtus/compare/v0.1.0...v0.2.0) # v0.1.0 2012-02-05 * [feature] New EmbeddedValue attribute (solnic) * [feature] Array and Set attributes support member coercions (emmanuel) * [feature] Support for scientific notation handling in string => integer coercion (dkubb) * [feature] Handling of string => numeric coercion with a leading + sign (dkubb) * [changed] Update Boolean coercion to handle "on", "off", "y", "n", "yes", "no" (dkubb) [Compare v0.0.10..v0.1.0](https://github.com/solnic/virtus/compare/v0.0.10...v0.1.0) # v0.0.10 2011-11-21 * [fixed] Default values are now duped on evaluate (rclosner) * [fixed] Allow to override attribute mutator methods (senny) [Compare v0.0.9..v0.0.10](https://github.com/solnic/virtus/compare/v0.0.9...v0.0.10) # v0.0.9 2011-10-11 * [fixed] Fix in type lookup for anonymous classes (dkubb) [Compare v0.0.8..v0.0.9](https://github.com/solnic/virtus/compare/v0.0.8...v0.0.9) # v0.0.8 2011-08-25 * [fixed] Fixed conflict with ActiveModel (RichGuk) * [changed] Renamed Coercion::String.to_class => Coercion::String.to_constant (emmanuel) [Compare v0.0.7..v0.0.8](https://github.com/solnic/virtus/compare/v0.0.7...v0.0.8) # v0.0.7 2011-07-31 * [BREAKING CHANGE] Attribute.primitive? has been removed (solnic) * [fixed] Added missing coercion_method setting to Virtus::Attribute::Object (solnic) * [general] Default value logic has been extracted into Attribute::DefaultValue class (solnic) * [added] Virtus::Attribute::Class (solnic) [Compare v0.0.6..v0.0.7](https://github.com/solnic/virtus/compare/v0.0.6...v0.0.7) # v0.0.6 2011-07-30 * [BREAKING CHANGE] Moved Virtus.determine_type to a shared module Virtus::TypeLookup (dkubb) * [BREAKING CHANGE] Attribute#typecast_to_primitive has been replaced by Attribute#coerce (solnic) * [BREAKING CHANGE] Attribute#typecast logic was moved to Attribute#set which is now a public method (solnic) * [feature] Added support for default values (solnic) * [general] Added custom inspect for Attribute classes (solnic) * [general] Added backports as a development dependency (dkubb) * [changed] Options API has been extracted from Attribute to a support module Virtus::Options (solnic) * [changed] Typecast classes have been replaced by a new hierarchy of Coercion classes like Coercion::String, Coercion::Integer etc. (solnic) * [changed] Attribute#get, #get!, #set, #set! & #coerce are now part of the public API (solnic) [Compare v0.0.5..v0.0.6](https://github.com/solnic/virtus/compare/v0.0.5...v0.0.6) # v0.0.5 2011-07-10 * [bugfix] Fixed DescendantsTracker + ActiveSupport collision (dkubb) [Compare v0.0.4..v0.0.5](https://github.com/solnic/virtus/compare/v0.0.4...v0.0.5) # v0.0.4 2011-07-08 * [BREAKING CHANGE] attributes hash has been replaced by a specialized class AttributeSet (dkubb) * [BREAKING CHANGE] Virtus::ClassMethods.attribute returns self instead of a created attribute (solnic) * [changed] descendants tracking has been extracted into DescendantsTracker module (dkubb) * [changed] Instance #primitive? method has been replaced by class utility method Virtus::Attribute.primitive? (solnic) * [changed] Virtus::Attribute::String#typecast_to_primitive delegates to Virtus::Typecast::String.call (solnic) [Compare v0.0.3..v0.0.4](https://github.com/solnic/virtus/compare/v0.0.3...v0.0.4) # v0.0.3 2011-06-09 * [BREAKING CHANGE] Attribute classes were moved to Virtus::Attribute namespace (solnic) * [BREAKING CHANGE] Attribute instance no longer holds the reference to a model (solnic) * [BREAKING CHANGE] #typecast no longer receives an instance of a model (override #set which calls #typecast if you need that) (solnic) * [changed] Adding reader/writer methods was moved from the attribute constructor to Virtus::ClassMethods.attribute (solnic) * [changed] Typecast logic has been moved into separate classes (see Virtus::Typecast) (solnic) * [added] Virtus::Attribute::DateTime#typecast supports objects which implement #to_datetime (solnic) * [general] Internals have been cleaned up, simplified and properly documented (solnic) [Compare v0.0.2..v0.0.3](https://github.com/solnic/virtus/compare/v0.0.2...v0.0.3) # v0.0.2 2011-06-06 * [bugfix] Fixed #typecast in custom attribute classes (solnic) [Compare v0.0.1..v0.0.2](https://github.com/solnic/virtus/compare/v0.0.1...v0.0.2) # v0.0.1 2011-06-04 First public release :)