Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/solnic/virtus
[DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects
https://github.com/solnic/virtus
Last synced: 3 days ago
JSON representation
[DISCONTINUED ] Attributes on Steroids for Plain Old Ruby Objects
- Host: GitHub
- URL: https://github.com/solnic/virtus
- Owner: solnic
- License: mit
- Created: 2011-04-02T16:23:50.000Z (over 13 years ago)
- Default Branch: master
- Last Pushed: 2021-08-10T13:39:34.000Z (over 3 years ago)
- Last Synced: 2024-12-03T00:05:03.844Z (10 days ago)
- Language: Ruby
- Homepage:
- Size: 1.77 MB
- Stars: 3,766
- Watchers: 64
- Forks: 229
- Open Issues: 71
-
Metadata Files:
- Readme: README.md
- Changelog: Changelog.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- awesome-ruby - Virtus - Attributes on Steroids for Plain Old Ruby Objects. (Core Extensions)
README
[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!
>
> @solnicVirtus
======[![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 prototypingAnd 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.modelattribute :name, String
attribute :age, Integer
attribute :birthday, DateTime
enduser = User.new(:name => 'Piotr', :age => 31)
user.attributes # => { :name => "Piotr", :age => 31, :birthday => nil }user.name # => "Piotr"
user.age = '31' # => 31
user.age.class # => Fixnumuser.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.modelattribute :name, String
enduser = 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
endUser.new(:name => 'Piotr')
# include just the attribute DSL
class User
include Virtus.model(:constructor => false, :mass_assignment => false)attribute :name, String
enduser = 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.moduleattribute :name, String
endmodule Age
include Virtus.module(:coerce => false)attribute :age, Integer
endclass User
include Name, Age
enduser = 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
enduser = User.new
user.extend(Virtus.model)
user.attribute :name, String
user.name = 'John'
user.name # => 'John'
```### Default Values
``` ruby
class Page
include Virtus.modelattribute :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_titledef default_editor_title
published? ? title : "UNPUBLISHED: #{title}"
end
endpage = 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.modelattribute :name, String
endclass Address
include Virtus.modelattribute :street, String
attribute :zipcode, String
attribute :city, City
endclass User
include Virtus.modelattribute :name, String
attribute :address, Address
enduser = 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.modelattribute :page_numbers, Array[Integer]
endbook = Book.new(:page_numbers => %w[1 2 3])
book.page_numbers # => [1, 2, 3]# Support EmbeddedValues, too!
class Address
include Virtus.modelattribute :address, String
attribute :locality, String
attribute :region, String
attribute :postal_code, String
endclass PhoneNumber
include Virtus.modelattribute :number, String
endclass User
include Virtus.modelattribute :phone_numbers, Array[PhoneNumber]
attribute :addresses, Set[Address]
enduser = 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.modelattribute :dimensions, Hash[Symbol => Float]
endpackage = 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.modelattribute :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.modelattribute :title, String
endclass Library
include Virtus.modelattribute :books, Array[Book]
endlibrary = 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.modelattribute :title, String
endclass BookCollection < Array
def <<(book)
if book.kind_of?(Hash)
super(Book.new(book))
else
super
end
end
endclass Library
include Virtus.modelattribute :books, BookCollection[Book]
endlibrary = Library.new
library.books << { :title => 'Another Introduction to Virtus' }
```### Value Objects
``` ruby
class GeoLocation
include Virtus.value_objectvalues do
attribute :latitude, Float
attribute :longitude, Float
end
endclass Venue
include Virtus.value_objectvalues do
attribute :name, String
attribute :location, GeoLocation
end
endvenue = 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
endclass User
include Virtus.modelattribute :info, Json, default: {}
enduser = User.new
user.info = '{"email":"[email protected]"}' # => {"email"=>"[email protected]"}
user.info.class # => Hash# With a custom attribute encapsulating coercion-specific configuration
class NoisyString < Virtus::Attribute
def coerce(value)
value.to_s.upcase
end
endclass User
include Virtus.modelattribute :scream, NoisyString
enduser = User.new(:scream => 'hello world!')
user.scream # => "HELLO WORLD!"
```### Private Attributes
``` ruby
class User
include Virtus.modelattribute :unique_id, String, :writer => :private
def set_unique_id(id)
self.unique_id = id
end
enduser = User.new(:unique_id => '1234-1234')
user.unique_id # => niluser.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.modelattribute :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
enduser = 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
endUser.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 YupNopeBooleansattribute :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 objectsRuby version support
--------------------Virtus is known to work correctly with the following rubies:
* 1.9.3
* 2.0.0
* 2.1.2
* jruby
* (probably) rbxCredits
-------* 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.