Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/davydovanton/shallow_attributes
Simple and lightweight Virtus analog.
https://github.com/davydovanton/shallow_attributes
data-object ruby virtus
Last synced: 11 days ago
JSON representation
Simple and lightweight Virtus analog.
- Host: GitHub
- URL: https://github.com/davydovanton/shallow_attributes
- Owner: davydovanton
- License: mit
- Created: 2016-03-14T19:58:31.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2023-04-18T17:52:34.000Z (over 1 year ago)
- Last Synced: 2024-04-25T17:21:42.894Z (7 months ago)
- Topics: data-object, ruby, virtus
- Language: Ruby
- Homepage:
- Size: 108 KB
- Stars: 100
- Watchers: 6
- Forks: 18
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# ShallowAttributes
[![Build Status](https://travis-ci.org/davydovanton/shallow_attributes.svg?branch=master)](https://travis-ci.org/davydovanton/shallow_attributes)
[![Code Climate](https://codeclimate.com/github/davydovanton/shallow_attributes/badges/gpa.svg)](https://codeclimate.com/github/davydovanton/shallow_attributes)
[![Coverage Status](https://coveralls.io/repos/github/davydovanton/shallow_attributes/badge.svg?branch=master)](https://coveralls.io/github/davydovanton/shallow_attributes?branch=master)
[![Inline docs](http://inch-ci.org/github/davydovanton/shallow_attributes.svg?branch=master)](http://inch-ci.org/github/davydovanton/shallow_attributes)Simple and lightweight Virtus analog without any dependencies. [Documentation][doc-link].
## Motivation
There are already a lot of good and flexible gems which solve a similar problem, allowing attributes
to be defined with their types, for example: [virtus][virtus-link], [fast_attributes][fast-attributes-link]
or [attrio][attrio-link]. However, the disadvantage of these gems is performance or API. So, the goal
of `ShallowAttributes` is to provide a simple solution which is similar to the `Virtus` API, simple, fast,
understandable and extendable.* This is [the performance benchmark][performance-benchmark] of ShallowAttributes compared to Virtus gems.
* [Default ruby struct, dry-struct, virtus and ShallowAttributes ips and memory benchmarks](https://gist.github.com/IvanShamatov/94e78ca52f04f20c6085651345dbdfda)## Installation
Add this line to your application's Gemfile:
``` ruby
gem 'shallow_attributes'
```And then execute:
$ bundle
Or install it yourself as:
$ gem install shallow_attributes
## Examples
### Table of contents
* [Using ShallowAttributes with Classes](#using-shallowattributes-with-classes)
* [Default Values](#default-values)
* [Mandatory Attributes](#mandatory-attributes)
* [Embedded Value](#embedded-value)
* [Custom Coercions](#custom-coercions)
* [Collection Member Coercions](#collection-member-coercions)
* [Note about Member Coercions](#important-note-about-member-coercions)
* [Overriding setters](#overriding-setters)
* [ActiveModel compatibility](#activemodel-compatibility)
* [Dry-types](#dry-types)### Using ShallowAttributes with Classes
You can create classes extended with Virtus and define attributes:
``` ruby
class User
include ShallowAttributesattribute :name, String
attribute :age, Integer
attribute :birthday, DateTime
endclass SuperUser < User
include ShallowAttributesattribute :name, String
attribute :age, Integer, allow_nil: true
attribute :birthday, DateTime
enduser = User.new(name: 'Anton', age: 31)
user.name # => "Anton"user.age = '31' # => 31
user.age = nil # => nil
user.age.class # => Fixnumuser.birthday = 'November 18th, 1983' # => #
user.attributes # => { name: "Anton", age: 31, birthday: nil }
# mass-assignment
user.attributes = { name: 'Jane', age: 21 }
user.name # => "Jane"
user.age # => 21super_user = SuperUser.new
user.age = nil # => 0
```ShallowAttributes doesn't make any assumptions about base classes. There is no need to define
default attributes, or even mix ShallowAttributes into the base class:``` ruby
require 'active_model'class Form
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Conversion
include ShallowAttributesdef persisted?
false
end
endclass SearchForm < Form
attribute :name, String
endform = SearchForm.new(name: 'Anton')
form.name # => "Anton"
```### Default Values
``` ruby
class Page
include ShallowAttributesattribute :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_titleprivate
def 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
```### Mandatory attributes
You can provide `present: true` option for any attribute that will prevent class from initialization
if this attribute was not provided:``` ruby
class CreditCard
include ShallowAttributes
attribute :number, Integer, present: true
attribute :owner, String, present: true
endcard = CreditCard.new(number: 1239342)
# => ShallowAttributes::MissingAttributeError: Mandatory attribute "owner" was not provided
```### Embedded Value
``` ruby
class City
include ShallowAttributesattribute :name, String
attribute :size, Integer, default: 9000
endclass Address
include ShallowAttributesattribute :street, String
attribute :zipcode, String, default: '111111'
attribute :city, City
endclass User
include ShallowAttributesattribute :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"
```### Custom Coercions
``` ruby
require 'json'class Json
def coerce(value, options = {})
value.is_a?(::Hash) ? value : JSON.parse(value)
end
endclass User
include ShallowAttributesattribute :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
def coerce(value, options = {})
value.to_s.upcase
end
endclass User
include ShallowAttributesattribute :scream, NoisyString
enduser = User.new(scream: 'hello world!')
user.scream # => "HELLO WORLD!"
```### Collection Member Coercions
``` ruby
# Support "primitive" classes
class Book
include ShallowAttributesattribute :page_numbers, Array, of: Integer
endbook = Book.new(:page_numbers => %w[1 2 3])
book.page_numbers # => [1, 2, 3]# Support EmbeddedValues, too!
class Address
include ShallowAttributesattribute :address, String
attribute :locality, String
attribute :region, String
attribute :postal_code, String
endclass PhoneNumber
include ShallowAttributesattribute :number, String
endclass User
include ShallowAttributesattribute :phone_numbers, Array, of: PhoneNumber
attribute :addresses, Array, of: 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 # => [#]user.attributes
# => {
# => :phone_numbers => [
# => { :number => '212-555-1212' },
# => { :number => '919-444-3265' }
# => ],
# => :addresses => [
# => {
# => :address => '1234 Any St.',
# => :locality => 'Anytown',
# => :region => "DC",
# => :postal_code => "21234"
# => }
# => ]
# => }
```### IMPORTANT note about member coercions
ShallowAttributes 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 ShallowAttributes
attribute :title, String
endclass Library
include ShallowAttributes
attribute :books, Array, of: Book
endlibrary = Library.new
# This will coerce Hash to a Book instance
library.books = [ { :title => 'Introduction' } ]# This WILL NOT COERCE the value because you mutate the books array with Array#<<
library.books << { :title => 'Another Introduction' }
```### Overriding setters
``` ruby
class User
include ShallowAttributesattribute :name, String
alias_method :_name=, :name=
def name=(new_name)
custom_name = nil
if new_name == "Godzilla"
custom_name = "Can't tell"
endself._name = custom_name || new_name
end
enduser = User.new(name: "Frank")
user.name # => 'Frank'user = User.new(name: "Godzilla")
user.name # => 'Can't tell'
```### ActiveModel compatibility
ShallowAttributes is fully compatible with ActiveModel.
#### Form object
``` ruby
require 'active_model'class SearchForm
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Conversion
include ShallowAttributesattribute :name, String
attribute :service_ids, Array, of: Integer
attribute :archived, 'Boolean', default: falsedef persisted?
false
enddef results
# ...
end
endclass SearchesController < ApplicationController
def index
search_params = params.require(:search_form).permit(...)
@search_form = SearchForm.new(search_params)
end
end
`````` erb
Search
<%= form_for @search_form do |f| %>
<%= f.text_field :name %>
<%= f.collection_check_boxes :service_ids, Service.all, :id, :name %>
<%= f.select :archived, [['Archived', true], ['Not Archived', false]] %>
<% end %>
```#### Validations
``` ruby
require 'active_model'class Children
include ShallowAttributes
include ActiveModel::Validationsattribute :scream, String
validates :scream, presence: true
enduser = User.new(scream: '')
user.valid? # => false
user.scream = 'hello world!'
user.valid? # => true
```### Dry-types
You can use dry-types objects as a type for your attribute:
```ruby
module Types
include Dry::Types.module
endclass User
include ShallowAttributesattribute :name, Types::Coercible::String
attribute :age, Types::Coercible::Int
attribute :birthday, DateTime
enduser = User.new(name: nil, age: 0)
user.name # => ''
user.age # => 0
```## Ruby version support
ShallowAttributes is [known to work correctly][travis-link] with the following rubies:
* 2.0
* 2.1
* 2.2
* 2.3
* 2.4
* jruby-headAlso we run rbx-2 buld too.
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/davydovanton/shallow_attributes.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected
to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
[doc-link]: http://www.rubydoc.info/github/davydovanton/shallow_attributes/master
[virtus-link]: https://github.com/solnic/virtus
[fast-attributes-link]: https://github.com/applift/fast_attributes
[attrio-link]: https://github.com/jetrockets/attrio
[performance-benchmark]: https://gist.github.com/davydovanton/d14b51ab63e3fab63ecb
[travis-link]: https://travis-ci.org/davydovanton/shallow_attributes