[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.