├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bench └── allocation.rb ├── lib ├── shallow_attributes.rb └── shallow_attributes │ ├── class_methods.rb │ ├── instance_methods.rb │ ├── type.rb │ ├── type │ ├── array.rb │ ├── boolean.rb │ ├── date.rb │ ├── date_time.rb │ ├── float.rb │ ├── integer.rb │ ├── string.rb │ └── time.rb │ └── version.rb ├── shallow_attributes.gemspec └── test ├── array_type_test.rb ├── boolean_type_test.rb ├── coercions_test.rb ├── custom_types_test.rb ├── date_time_type_test.rb ├── date_type_test.rb ├── dry_types_test.rb ├── float_type_test.rb ├── inheritance_test.rb ├── integer_type_test.rb ├── overriding_setters_test.rb ├── present_option_test.rb ├── shallow_attributes_test.rb ├── string_type_test.rb ├── test_helper.rb ├── time_type_test.rb └── types_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | coverage 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | before_install: 5 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true 6 | - gem install bundler -v '< 2' 7 | rvm: 8 | - 2.3.1 9 | - 2.4.0 10 | - 2.5.0 11 | - 2.6.0 12 | - jruby-head 13 | - rbx-2 14 | - ruby-head 15 | matrix: 16 | allow_failures: 17 | - rvm: rbx-2 18 | - rvm: ruby-head 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ShallowAttributes Changes 2 | ## HEAD 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at antondavydov.o@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in shallow_attributes.gemspec 4 | gemspec 5 | 6 | if !ENV['TRAVIS'] 7 | gem 'yard', require: false 8 | end 9 | 10 | gem 'coveralls', require: false 11 | gem 'json', '1.8.5' 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anton Davydov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShallowAttributes 2 | 3 | [![Build Status](https://travis-ci.org/davydovanton/shallow_attributes.svg?branch=master)](https://travis-ci.org/davydovanton/shallow_attributes) 4 | [![Code Climate](https://codeclimate.com/github/davydovanton/shallow_attributes/badges/gpa.svg)](https://codeclimate.com/github/davydovanton/shallow_attributes) 5 | [![Coverage Status](https://coveralls.io/repos/github/davydovanton/shallow_attributes/badge.svg?branch=master)](https://coveralls.io/github/davydovanton/shallow_attributes?branch=master) 6 | [![Inline docs](http://inch-ci.org/github/davydovanton/shallow_attributes.svg?branch=master)](http://inch-ci.org/github/davydovanton/shallow_attributes) 7 | 8 | Simple and lightweight Virtus analog without any dependencies. [Documentation][doc-link]. 9 | 10 | ## Motivation 11 | 12 | There are already a lot of good and flexible gems which solve a similar problem, allowing attributes 13 | to be defined with their types, for example: [virtus][virtus-link], [fast_attributes][fast-attributes-link] 14 | or [attrio][attrio-link]. However, the disadvantage of these gems is performance or API. So, the goal 15 | of `ShallowAttributes` is to provide a simple solution which is similar to the `Virtus` API, simple, fast, 16 | understandable and extendable. 17 | 18 | * This is [the performance benchmark][performance-benchmark] of ShallowAttributes compared to Virtus gems. 19 | * [Default ruby struct, dry-struct, virtus and ShallowAttributes ips and memory benchmarks](https://gist.github.com/IvanShamatov/94e78ca52f04f20c6085651345dbdfda) 20 | 21 | ## Installation 22 | 23 | Add this line to your application's Gemfile: 24 | 25 | ``` ruby 26 | gem 'shallow_attributes' 27 | ``` 28 | 29 | And then execute: 30 | 31 | $ bundle 32 | 33 | Or install it yourself as: 34 | 35 | $ gem install shallow_attributes 36 | 37 | ## Examples 38 | 39 | ### Table of contents 40 | 41 | * [Using ShallowAttributes with Classes](#using-shallowattributes-with-classes) 42 | * [Default Values](#default-values) 43 | * [Mandatory Attributes](#mandatory-attributes) 44 | * [Embedded Value](#embedded-value) 45 | * [Custom Coercions](#custom-coercions) 46 | * [Collection Member Coercions](#collection-member-coercions) 47 | * [Note about Member Coercions](#important-note-about-member-coercions) 48 | * [Overriding setters](#overriding-setters) 49 | * [ActiveModel compatibility](#activemodel-compatibility) 50 | * [Dry-types](#dry-types) 51 | 52 | ### Using ShallowAttributes with Classes 53 | 54 | You can create classes extended with Virtus and define attributes: 55 | 56 | ``` ruby 57 | class User 58 | include ShallowAttributes 59 | 60 | attribute :name, String 61 | attribute :age, Integer 62 | attribute :birthday, DateTime 63 | end 64 | 65 | class SuperUser < User 66 | include ShallowAttributes 67 | 68 | attribute :name, String 69 | attribute :age, Integer, allow_nil: true 70 | attribute :birthday, DateTime 71 | end 72 | 73 | user = User.new(name: 'Anton', age: 31) 74 | user.name # => "Anton" 75 | 76 | user.age = '31' # => 31 77 | user.age = nil # => nil 78 | user.age.class # => Fixnum 79 | 80 | user.birthday = 'November 18th, 1983' # => # 81 | 82 | user.attributes # => { name: "Anton", age: 31, birthday: nil } 83 | 84 | # mass-assignment 85 | user.attributes = { name: 'Jane', age: 21 } 86 | user.name # => "Jane" 87 | user.age # => 21 88 | 89 | super_user = SuperUser.new 90 | user.age = nil # => 0 91 | ``` 92 | 93 | ShallowAttributes doesn't make any assumptions about base classes. There is no need to define 94 | default attributes, or even mix ShallowAttributes into the base class: 95 | 96 | ``` ruby 97 | require 'active_model' 98 | 99 | class Form 100 | extend ActiveModel::Naming 101 | extend ActiveModel::Translation 102 | include ActiveModel::Conversion 103 | include ShallowAttributes 104 | 105 | def persisted? 106 | false 107 | end 108 | end 109 | 110 | class SearchForm < Form 111 | attribute :name, String 112 | end 113 | 114 | form = SearchForm.new(name: 'Anton') 115 | form.name # => "Anton" 116 | ``` 117 | 118 | ### Default Values 119 | 120 | ``` ruby 121 | class Page 122 | include ShallowAttributes 123 | 124 | attribute :title, String 125 | 126 | # default from a singleton value (integer in this case) 127 | attribute :views, Integer, default: 0 128 | 129 | # default from a singleton value (boolean in this case) 130 | attribute :published, 'Boolean', default: false 131 | 132 | # default from a callable object (proc in this case) 133 | attribute :slug, String, default: lambda { |page, attribute| page.title.downcase.gsub(' ', '-') } 134 | 135 | # default from a method name as symbol 136 | attribute :editor_title, String, default: :default_editor_title 137 | 138 | private 139 | 140 | def default_editor_title 141 | published ? title : "UNPUBLISHED: #{title}" 142 | end 143 | end 144 | 145 | page = Page.new(title: 'Virtus README') 146 | page.slug # => 'virtus-readme' 147 | page.views # => 0 148 | page.published # => false 149 | page.editor_title # => "UNPUBLISHED: Virtus README" 150 | 151 | page.views = 10 152 | page.views # => 10 153 | page.reset_attribute(:views) # => 0 154 | page.views # => 0 155 | ``` 156 | 157 | ### Mandatory attributes 158 | You can provide `present: true` option for any attribute that will prevent class from initialization 159 | if this attribute was not provided: 160 | 161 | ``` ruby 162 | class CreditCard 163 | include ShallowAttributes 164 | attribute :number, Integer, present: true 165 | attribute :owner, String, present: true 166 | end 167 | 168 | card = CreditCard.new(number: 1239342) 169 | # => ShallowAttributes::MissingAttributeError: Mandatory attribute "owner" was not provided 170 | ``` 171 | 172 | 173 | ### Embedded Value 174 | 175 | ``` ruby 176 | class City 177 | include ShallowAttributes 178 | 179 | attribute :name, String 180 | attribute :size, Integer, default: 9000 181 | end 182 | 183 | class Address 184 | include ShallowAttributes 185 | 186 | attribute :street, String 187 | attribute :zipcode, String, default: '111111' 188 | attribute :city, City 189 | end 190 | 191 | class User 192 | include ShallowAttributes 193 | 194 | attribute :name, String 195 | attribute :address, Address 196 | end 197 | 198 | user = User.new(address: { 199 | street: 'Street 1/2', 200 | zipcode: '12345', 201 | city: { 202 | name: 'NYC' 203 | } 204 | }) 205 | 206 | user.address.street # => "Street 1/2" 207 | user.address.city.name # => "NYC" 208 | ``` 209 | 210 | ### Custom Coercions 211 | 212 | ``` ruby 213 | require 'json' 214 | 215 | class Json 216 | def coerce(value, options = {}) 217 | value.is_a?(::Hash) ? value : JSON.parse(value) 218 | end 219 | end 220 | 221 | class User 222 | include ShallowAttributes 223 | 224 | attribute :info, Json, default: {} 225 | end 226 | 227 | user = User.new 228 | user.info = '{"email":"john@domain.com"}' # => {"email"=>"john@domain.com"} 229 | user.info.class # => Hash 230 | 231 | # With a custom attribute encapsulating coercion-specific configuration 232 | class NoisyString 233 | def coerce(value, options = {}) 234 | value.to_s.upcase 235 | end 236 | end 237 | 238 | class User 239 | include ShallowAttributes 240 | 241 | attribute :scream, NoisyString 242 | end 243 | 244 | user = User.new(scream: 'hello world!') 245 | user.scream # => "HELLO WORLD!" 246 | ``` 247 | 248 | ### Collection Member Coercions 249 | 250 | ``` ruby 251 | # Support "primitive" classes 252 | class Book 253 | include ShallowAttributes 254 | 255 | attribute :page_numbers, Array, of: Integer 256 | end 257 | 258 | book = Book.new(:page_numbers => %w[1 2 3]) 259 | book.page_numbers # => [1, 2, 3] 260 | 261 | # Support EmbeddedValues, too! 262 | class Address 263 | include ShallowAttributes 264 | 265 | attribute :address, String 266 | attribute :locality, String 267 | attribute :region, String 268 | attribute :postal_code, String 269 | end 270 | 271 | class PhoneNumber 272 | include ShallowAttributes 273 | 274 | attribute :number, String 275 | end 276 | 277 | class User 278 | include ShallowAttributes 279 | 280 | attribute :phone_numbers, Array, of: PhoneNumber 281 | attribute :addresses, Array, of: Address 282 | end 283 | 284 | user = User.new( 285 | :phone_numbers => [ 286 | { :number => '212-555-1212' }, 287 | { :number => '919-444-3265' } ], 288 | :addresses => [ 289 | { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" } ]) 290 | 291 | user.phone_numbers # => [#, #] 292 | user.addresses # => [#] 293 | 294 | user.attributes 295 | # => { 296 | # => :phone_numbers => [ 297 | # => { :number => '212-555-1212' }, 298 | # => { :number => '919-444-3265' } 299 | # => ], 300 | # => :addresses => [ 301 | # => { 302 | # => :address => '1234 Any St.', 303 | # => :locality => 'Anytown', 304 | # => :region => "DC", 305 | # => :postal_code => "21234" 306 | # => } 307 | # => ] 308 | # => } 309 | ``` 310 | 311 | ### IMPORTANT note about member coercions 312 | 313 | ShallowAttributes performs coercions only when a value is being assigned. If you mutate the value 314 | later on using its own interfaces then coercion won't be triggered. 315 | 316 | Here's an example: 317 | 318 | ``` ruby 319 | class Book 320 | include ShallowAttributes 321 | attribute :title, String 322 | end 323 | 324 | class Library 325 | include ShallowAttributes 326 | attribute :books, Array, of: Book 327 | end 328 | 329 | library = Library.new 330 | 331 | # This will coerce Hash to a Book instance 332 | library.books = [ { :title => 'Introduction' } ] 333 | 334 | # This WILL NOT COERCE the value because you mutate the books array with Array#<< 335 | library.books << { :title => 'Another Introduction' } 336 | ``` 337 | 338 | ### Overriding setters 339 | 340 | ``` ruby 341 | class User 342 | include ShallowAttributes 343 | 344 | attribute :name, String 345 | 346 | alias_method :_name=, :name= 347 | def name=(new_name) 348 | custom_name = nil 349 | if new_name == "Godzilla" 350 | custom_name = "Can't tell" 351 | end 352 | 353 | self._name = custom_name || new_name 354 | end 355 | end 356 | 357 | user = User.new(name: "Frank") 358 | user.name # => 'Frank' 359 | 360 | user = User.new(name: "Godzilla") 361 | user.name # => 'Can't tell' 362 | ``` 363 | 364 | ### ActiveModel compatibility 365 | 366 | ShallowAttributes is fully compatible with ActiveModel. 367 | 368 | #### Form object 369 | 370 | ``` ruby 371 | require 'active_model' 372 | 373 | class SearchForm 374 | extend ActiveModel::Naming 375 | extend ActiveModel::Translation 376 | include ActiveModel::Conversion 377 | include ShallowAttributes 378 | 379 | attribute :name, String 380 | attribute :service_ids, Array, of: Integer 381 | attribute :archived, 'Boolean', default: false 382 | 383 | def persisted? 384 | false 385 | end 386 | 387 | def results 388 | # ... 389 | end 390 | end 391 | 392 | class SearchesController < ApplicationController 393 | def index 394 | search_params = params.require(:search_form).permit(...) 395 | @search_form = SearchForm.new(search_params) 396 | end 397 | end 398 | ``` 399 | 400 | ``` erb 401 |

Search

402 | <%= form_for @search_form do |f| %> 403 | <%= f.text_field :name %> 404 | <%= f.collection_check_boxes :service_ids, Service.all, :id, :name %> 405 | <%= f.select :archived, [['Archived', true], ['Not Archived', false]] %> 406 | <% end %> 407 | ``` 408 | 409 | #### Validations 410 | 411 | ``` ruby 412 | require 'active_model' 413 | 414 | class Children 415 | include ShallowAttributes 416 | include ActiveModel::Validations 417 | 418 | attribute :scream, String 419 | validates :scream, presence: true 420 | end 421 | 422 | user = User.new(scream: '') 423 | user.valid? # => false 424 | user.scream = 'hello world!' 425 | user.valid? # => true 426 | ``` 427 | 428 | ### Dry-types 429 | You can use dry-types objects as a type for your attribute: 430 | ```ruby 431 | module Types 432 | include Dry::Types.module 433 | end 434 | 435 | class User 436 | include ShallowAttributes 437 | 438 | attribute :name, Types::Coercible::String 439 | attribute :age, Types::Coercible::Int 440 | attribute :birthday, DateTime 441 | end 442 | 443 | user = User.new(name: nil, age: 0) 444 | user.name # => '' 445 | user.age # => 0 446 | ``` 447 | 448 | ## Ruby version support 449 | 450 | ShallowAttributes is [known to work correctly][travis-link] with the following rubies: 451 | 452 | * 2.0 453 | * 2.1 454 | * 2.2 455 | * 2.3 456 | * 2.4 457 | * jruby-head 458 | 459 | Also we run rbx-2 buld too. 460 | 461 | ## Contributing 462 | 463 | Bug reports and pull requests are welcome on GitHub at https://github.com/davydovanton/shallow_attributes. 464 | This project is intended to be a safe, welcoming space for collaboration, and contributors are expected 465 | to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 466 | 467 | ## License 468 | 469 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 470 | 471 | [doc-link]: http://www.rubydoc.info/github/davydovanton/shallow_attributes/master 472 | [virtus-link]: https://github.com/solnic/virtus 473 | [fast-attributes-link]: https://github.com/applift/fast_attributes 474 | [attrio-link]: https://github.com/jetrockets/attrio 475 | [performance-benchmark]: https://gist.github.com/davydovanton/d14b51ab63e3fab63ecb 476 | [travis-link]: https://travis-ci.org/davydovanton/shallow_attributes 477 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /bench/allocation.rb: -------------------------------------------------------------------------------- 1 | require 'memory_profiler' 2 | require 'shallow_attributes' 3 | 4 | hash = { addresses: [ { street: 'Street 1/2', city: { name: 'NYC' } }, { street: 'Street 3/2', city: { name: 'Moscow' } } ] } 5 | class City 6 | include ShallowAttributes 7 | attribute :name, String 8 | attribute :size, Integer, default: 9000 9 | end 10 | class Address 11 | include ShallowAttributes 12 | attribute :street, String 13 | attribute :zipcode, String, default: '111111' 14 | attribute :city, City 15 | end 16 | class Person 17 | include ShallowAttributes 18 | attribute :name, String 19 | attribute :address, Address 20 | attribute :addresses, Array, of: Address 21 | end 22 | report = MemoryProfiler.report { 1_000.times { Person.new(hash) } } 23 | 24 | puts report.pretty_print 25 | 26 | # allocated memory by gem 27 | # ----------------------------------- 28 | # 6830520 shallow_attributes/lib 29 | # 30 | # allocated memory by gem 31 | # ----------------------------------- 32 | # 6310752 shallow_attributes/lib 33 | # 34 | # allocated memory by gem 35 | # ----------------------------------- 36 | # 5790752 shallow_attributes/lib 37 | # 38 | # allocated memory by gem 39 | # ----------------------------------- 40 | # 5150752 shallow_attributes/lib 41 | # 42 | # allocated memory by gem 43 | # ----------------------------------- 44 | # 4950752 shallow_attributes/lib 45 | # 46 | # allocated memory by gem 47 | # ----------------------------------- 48 | # 4550752 shallow_attributes/lib 49 | # 50 | # allocated memory by gem 51 | # ----------------------------------- 52 | # 4550520 shallow_attributes/lib 53 | # 54 | # allocated memory by gem 55 | # ----------------------------------- 56 | # 4470752 shallow_attributes/lib 57 | -------------------------------------------------------------------------------- /lib/shallow_attributes.rb: -------------------------------------------------------------------------------- 1 | require 'shallow_attributes/class_methods' 2 | require 'shallow_attributes/instance_methods' 3 | require 'shallow_attributes/type' 4 | require 'shallow_attributes/version' 5 | 6 | # Main module 7 | # 8 | # @since 0.1.0 9 | module ShallowAttributes 10 | include InstanceMethods 11 | 12 | # Error class for mandatory arguments that were not provided 13 | # 14 | # @since 0.10.0 15 | class MissingAttributeError < TypeError; end 16 | 17 | # Including ShallowAttributes class methods to specific class 18 | # 19 | # @private 20 | # 21 | # @param [Class] base the class containing class methods 22 | # 23 | # @return [Class] class for including ShallowAttributes gem 24 | # 25 | # @since 0.1.0 26 | def self.included(base) 27 | base.extend(ClassMethods) 28 | end 29 | end 30 | 31 | # Boolean class for working with bool values 32 | # 33 | # @private 34 | # 35 | # @since 0.1.0 36 | class Boolean; end 37 | -------------------------------------------------------------------------------- /lib/shallow_attributes/class_methods.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | # Abstract class for value classes. Provides some helper methods for 3 | # working with class methods. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | module ClassMethods 9 | # Inject our default values into subclasses. 10 | # 11 | # @private 12 | # 13 | # @param [Object] subclass 14 | # 15 | def inherited(subclass) 16 | super 17 | if respond_to?(:default_values) 18 | subclass.default_values.merge!(default_values) 19 | end 20 | end 21 | 22 | # Returns hash that contains default values for each attribute 23 | # 24 | # @private 25 | # 26 | # @return [Hash] hash with default values 27 | # 28 | # @since 0.1.0 29 | def default_values 30 | @default_values ||= {} 31 | end 32 | 33 | # Returns hash that contains mandatory attributes 34 | # 35 | # @private 36 | # 37 | # @return [Hash] hash with mandatory attributes 38 | # 39 | # @since 0.10.0 40 | def mandatory_attributes 41 | @mandatory_attributes ||= {} 42 | end 43 | 44 | # Returns all class attributes. 45 | # 46 | # @example Create new User instance 47 | # class User 48 | # include ShallowAttributes 49 | # attribute :name, String 50 | # attribute :last_name, String 51 | # attribute :age, Integer 52 | # end 53 | # 54 | # User.attributes # => [:name, :last_name, :age] 55 | # 56 | # @return [Hash] 57 | # 58 | # @since 0.1.0 59 | def attributes 60 | default_values.keys 61 | end 62 | 63 | # Define attribute with specific type and default value 64 | # for current class. 65 | # 66 | # @param [String, Symbol] name the attribute name 67 | # @param [String, Symbol] type the type of attribute 68 | # @param [hash] options the attribute options 69 | # @option options [Object] :default default value for attribute 70 | # @option options [Class] :of class of array elems 71 | # @option options [boolean] :allow_nil cast `nil` to integer or float 72 | # @option options [boolean] :present raise error if attribute was not provided 73 | # 74 | # @example Create new User instance 75 | # class User 76 | # include ShallowAttributes 77 | # attribute :name, String, default: 'Anton' 78 | # end 79 | # 80 | # User.new # => #"Anton"}, @name="Anton"> 81 | # User.new(name: 'ben') # => #"Ben"}, @name="Ben"> 82 | # 83 | # @return [Object] 84 | # 85 | # @since 0.1.0 86 | def attribute(name, type, options = {}) 87 | options[:default] ||= [] if type == Array 88 | 89 | default_values[name] = options.delete(:default) 90 | mandatory_attributes[name] = options.delete(:present) 91 | 92 | initialize_setter(name, type, options) 93 | initialize_getter(name) 94 | end 95 | 96 | private 97 | 98 | # Define setter method for each attribute. 99 | # 100 | # @private 101 | # 102 | # @param [String, Symbol] name the attribute name 103 | # @param [String, Symbol] type the type of attribute 104 | # @param [hash] options the attribute options 105 | # 106 | # @return [Object] 107 | # 108 | # @since 0.1.0 109 | def initialize_setter(name, type, options) 110 | type_class = dry_type?(type) ? type.class : type 111 | 112 | module_eval <<-EOS, __FILE__, __LINE__ + 1 113 | def #{name}=(value) 114 | @#{name} = if value.is_a?(#{type_class}) && !value.is_a?(Array) 115 | value 116 | else 117 | #{type_casting(type, options)} 118 | end 119 | 120 | @attributes[:#{name}] = @#{name} 121 | end 122 | EOS 123 | end 124 | 125 | # Define getter method for each attribute. 126 | # 127 | # @private 128 | # 129 | # @param [String, Symbol] name the attribute name 130 | # 131 | # @return [Object] 132 | # 133 | # @since 0.1.0 134 | def initialize_getter(name) 135 | attr_reader name 136 | end 137 | 138 | private 139 | 140 | DRY_TYPE_CLASS = 'Dry::Types'.freeze 141 | 142 | # Check type with dry-type 143 | # 144 | # @private 145 | # 146 | # @param [Class] type the class of type 147 | # 148 | # @return [Bool] 149 | # 150 | # @since 0.2.0 151 | def dry_type?(type) 152 | type.class.name.match(DRY_TYPE_CLASS) 153 | end 154 | 155 | # Returns string for type casting 156 | # 157 | # @private 158 | # 159 | # @param [Class] type the class of type 160 | # @param [Hash] options the options 161 | # 162 | # @return [String] 163 | # 164 | # @since 0.2.0 165 | def type_casting(type, options) 166 | if dry_type?(type) 167 | # yep, I know that it's terrible line but it was the easily 168 | # way to type cast data with dry-types from class method in instance method 169 | "ObjectSpace._id2ref(#{type.object_id})[value]" 170 | else 171 | "ShallowAttributes::Type.coerce(#{type}, value, #{options})" 172 | end 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/shallow_attributes/instance_methods.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | # Abstract class for value classes. Provides some helper methods for 3 | # working with attributes. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | module InstanceMethods 9 | # Lambda object for getting attributes hash for a specific 10 | # value object. 11 | # 12 | # @private 13 | # 14 | # @since 0.1.0 15 | TO_H_PROC = ->(value) { value.respond_to?(:to_hash) ? value.to_hash : value } 16 | 17 | # Initialize an instance object with specific attributes 18 | # 19 | # @param [Hash] attrs the attributes contained in the class 20 | # 21 | # @example Create new User instance 22 | # class User 23 | # include ShallowAttributes 24 | # attribute :name, String 25 | # end 26 | # 27 | # User.new(name: 'Anton') # => #"Anton"}, @name="Anton"> 28 | # 29 | # @return the new instance of value class with specific attributes 30 | # 31 | # @since 0.1.0 32 | def initialize(attrs = {}) 33 | @attributes = {} 34 | attrs.each_pair do |key, value| 35 | key = key.to_sym 36 | @attributes[key] = value if default_values.key?(key) 37 | end 38 | define_attributes 39 | define_default_attributes 40 | define_mandatory_attributes 41 | end 42 | 43 | # Returns hash of object attributes 44 | # 45 | # @example Returns all user attributes 46 | # class User 47 | # include ShallowAttributes 48 | # attribute :name, String 49 | # end 50 | # 51 | # user = User.new(name: 'Anton') 52 | # user.attributes # => { name: "Anton" } 53 | # 54 | # @return [Hash] 55 | # 56 | # @since 0.1.0 57 | def attributes 58 | hash = {} 59 | @attributes.map do |key, value| 60 | hash[key] = 61 | value.is_a?(Array) ? value.map(&TO_H_PROC) : TO_H_PROC.call(value) 62 | end 63 | hash 64 | end 65 | 66 | # @since 0.1.0 67 | alias_method :to_h, :attributes 68 | 69 | # @since 0.1.0 70 | alias_method :to_hash, :attributes 71 | 72 | # Attribute values mass-assignment 73 | # 74 | # @param [Hash] attributes the attributes which will be assignment 75 | # 76 | # @example Assignment new user name 77 | # class User 78 | # include ShallowAttributes 79 | # attribute :name, String 80 | # end 81 | # 82 | # user = User.new(name: 'Anton') 83 | # user.attributes = { name: "Ben" } 84 | # user.attributes # => { name: "Ben" } 85 | # 86 | # @return [Hash] attributes hash 87 | # 88 | # @since 0.1.0 89 | def attributes=(attributes) 90 | attributes.each_pair do |key, value| 91 | @attributes[key.to_sym] = value 92 | end 93 | define_attributes 94 | end 95 | 96 | # Reset specific attribute to default value. 97 | # 98 | # @param [Symbol] attribute the attribute which will be reset 99 | # 100 | # @example Reset name value 101 | # class User 102 | # include ShallowAttributes 103 | # attribute :name, String, default: 'Ben' 104 | # end 105 | # 106 | # user = User.new(name: 'Anton') 107 | # user.reset_attribute(:name) 108 | # user.attributes # => { name: "Ben" } 109 | # 110 | # @return the last attribute value 111 | # 112 | # @since 0.1.0 113 | def reset_attribute(attribute) 114 | instance_variable_set("@#{attribute}", default_value_for(attribute)) 115 | end 116 | 117 | # Sets new values and returns self. Needs for embedded value. 118 | # 119 | # @private 120 | # 121 | # @param [Hash] value the new attributes for current object 122 | # @param [Hash] _options 123 | # 124 | # @example Use embedded values 125 | # class User 126 | # include ShallowAttributes 127 | # attribute :name, String, default: 'Ben' 128 | # end 129 | # 130 | # class Post 131 | # include ShallowAttributes 132 | # attribute :author, User 133 | # end 134 | # 135 | # post = Post.new(author: { name: 'Anton'} ) 136 | # post.user.name # => 'Anton' 137 | # 138 | # @return the object 139 | # 140 | # @since 0.1.0 141 | def coerce(value, _options = {}) 142 | self.attributes = value 143 | self 144 | end 145 | 146 | # Compare values of two objects 147 | # 148 | # @param [Object] object the other object 149 | # 150 | # @example Compare two value objects 151 | # class User 152 | # include ShallowAttributes 153 | # attribute :name, String, default: 'Ben' 154 | # end 155 | # 156 | # user1 = User.new(name: 'Anton') 157 | # user2 = User.new(name: 'Anton') 158 | # user1 == user2 # => true 159 | # 160 | # @return [boolean] 161 | # 162 | # @since 0.1.0 163 | def ==(object) 164 | self.to_h == object.to_h 165 | end 166 | 167 | # Inspect instance object 168 | # 169 | # @example Inspect the object 170 | # class User 171 | # include ShallowAttributes 172 | # attribute :name, String, default: 'Ben' 173 | # end 174 | # 175 | # user = User.new(name: 'Anton') 176 | # user.inspect # => "#" 177 | # 178 | # @return [String] 179 | # 180 | # @since 0.1.0 181 | def inspect 182 | "#<#{self.class}#{attributes.map{ |k, v| " #{k}=#{v.inspect}" }.join}>" 183 | end 184 | 185 | private 186 | 187 | # Define default values for attributes. 188 | # 189 | # @private 190 | # 191 | # @return the object 192 | # 193 | # @since 0.1.0 194 | def define_default_attributes 195 | default_values.each do |key, value| 196 | next unless @attributes[key].nil? && !value.nil? 197 | send("#{key}=", default_value_for(key)) 198 | end 199 | end 200 | 201 | # Define mandatory attributes for object and raise exception 202 | # if they were not provided 203 | # 204 | # @private 205 | # 206 | # @raise [MissingAttributeError] if attribute was not provided 207 | # 208 | # @return the object 209 | # 210 | # @since 0.10.0 211 | def define_mandatory_attributes 212 | mandatory_attributes.each do |key, value| 213 | next unless @attributes[key].nil? && value 214 | raise ShallowAttributes::MissingAttributeError, %(Mandatory attribute "#{key}" was not provided) 215 | end 216 | end 217 | 218 | # Define attributes from `@attributes` instance value. 219 | # 220 | # @private 221 | # 222 | # @return the object 223 | # 224 | # @since 0.1.0 225 | def define_attributes 226 | @attributes.each do |key, value| 227 | send("#{key}=", value) 228 | end 229 | end 230 | 231 | # Returns default value for specific attribute. Default values hash 232 | # is taken from class getter `default_values`. 233 | # 234 | # @private 235 | # 236 | # @return [nil] if default value not defined 237 | # @return [Object] if default value is defined 238 | # 239 | # @since 0.1.0 240 | def default_value_for(attribute) 241 | value = default_values[attribute] 242 | 243 | case value 244 | when Proc 245 | value.call(self, attribute) 246 | when Symbol, String 247 | respond_to?(value, true) ? send(value) : value 248 | else 249 | value 250 | end 251 | end 252 | 253 | # Returns hash of default class values 254 | # 255 | # @private 256 | # 257 | # @return [Hash] 258 | # 259 | # @since 0.1.0 260 | def default_values 261 | @default_values ||= self.class.default_values 262 | end 263 | 264 | # Returns hash of mandatory class attributes 265 | # 266 | # @private 267 | # 268 | # @return [Hash] 269 | # 270 | # @since 0.10.0 271 | def mandatory_attributes 272 | @mandatory_attributes ||= self.class.mandatory_attributes 273 | end 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type.rb: -------------------------------------------------------------------------------- 1 | require 'shallow_attributes/type/array' 2 | require 'shallow_attributes/type/boolean' 3 | require 'shallow_attributes/type/date_time' 4 | require 'shallow_attributes/type/float' 5 | require 'shallow_attributes/type/integer' 6 | require 'shallow_attributes/type/string' 7 | require 'shallow_attributes/type/time' 8 | require 'shallow_attributes/type/date' 9 | 10 | module ShallowAttributes 11 | # Namespace for standard type classes 12 | # 13 | # @since 0.1.0 14 | module Type 15 | # Error class for invalid value types 16 | # 17 | # @since 0.1.0 18 | class InvalidValueError < TypeError 19 | end 20 | 21 | # Hash object with cached type objects. 22 | # 23 | # @private 24 | # 25 | # @since 0.1.0 26 | DEFAULT_TYPE_OBJECTS = { 27 | ::Array => ShallowAttributes::Type::Array.new, 28 | ::DateTime => ShallowAttributes::Type::DateTime.new, 29 | ::Float => ShallowAttributes::Type::Float.new, 30 | ::Integer => ShallowAttributes::Type::Integer.new, 31 | ::String => ShallowAttributes::Type::String.new, 32 | ::Time => ShallowAttributes::Type::Time.new, 33 | ::Date => ShallowAttributes::Type::Date.new 34 | }.freeze 35 | 36 | class << self 37 | # Convert value object to specific Type class 38 | # 39 | # @private 40 | # 41 | # @param [Class] type the type class object 42 | # @param [Object] value the value that should be coerced to the necessary type 43 | # @param [Hash] options the options to create a message with. 44 | # @option options [String] :of The type of array 45 | # @option options [boolean] :allow_nil cast `nil` to integer or float 46 | # 47 | # @example Convert integer to sting type 48 | # ShallowAttributes::Type.coerce(String, 1) 49 | # # => '1' 50 | # 51 | # @example Convert string to custom hash type 52 | # ShallowAttributes::Type.instance_for(JsonType, '{"a"=>1}') 53 | # # => { a: 1 } 54 | # 55 | # @return the converted value object 56 | # 57 | # @since 0.1.0 58 | def coerce(type, value, options = {}) 59 | type_instance(type).coerce(value, options) 60 | end 61 | 62 | private 63 | 64 | # Returns class object for specific Type class 65 | # 66 | # @private 67 | # 68 | # @param [Class] klass the type class object 69 | # 70 | # @example Returns Sting type class 71 | # ShallowAttributes::Type.instance_for(String) 72 | # # => ShallowAttributes::Type::Sting class 73 | # 74 | # @example Returns other type class 75 | # ShallowAttributes::Type.instance_for(MySpecialStringType) 76 | # # => MySpecialStringType class 77 | # 78 | # @return [Class] 79 | # 80 | # @since 0.1.0 81 | def type_instance(klass) 82 | DEFAULT_TYPE_OBJECTS[klass] || ShallowAttributes::Type.const_get(klass.name).new 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/array.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # Abstract class for typecast object to Array type. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Array 9 | # Convert value to Array type 10 | # 11 | # @private 12 | # 13 | # @param [Array] values 14 | # @param [Hash] options 15 | # @option options [String] :of the type of array element class 16 | # 17 | # @example Convert integer array to string array 18 | # ShallowAttributes::Type::Array.new.coerce([1, 2], String) 19 | # # => ['1', '2'] 20 | # 21 | # @raise [InvalidValueError] if value is not an Array 22 | # 23 | # @return [Array] 24 | # 25 | # @since 0.1.0 26 | def coerce(values, options = {}) 27 | unless values.is_a? ::Array 28 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{values}" for type "Array") 29 | end 30 | values.map! do |value| 31 | ShallowAttributes::Type.coerce(item_klass(options[:of]), value) 32 | end 33 | end 34 | 35 | private 36 | 37 | def item_klass(klass) 38 | return klass unless klass.is_a? ::String 39 | Object.const_get(klass) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/boolean.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # Abstract class for typecast object to Boolean type. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Boolean 9 | # Array of true values 10 | # 11 | # @private 12 | # 13 | # @since 0.1.0 14 | TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].freeze 15 | 16 | # Array of false values 17 | # 18 | # @private 19 | # 20 | # @since 0.1.0 21 | FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', nil].freeze 22 | 23 | # Convert value to Boolean type 24 | # 25 | # @private 26 | # 27 | # @param [Object] value 28 | # @param [Hash] _options 29 | # 30 | # @example Convert integer to boolean value 31 | # ShallowAttributes::Type::Boolean.new.coerce(1) 32 | # # => true 33 | # 34 | # ShallowAttributes::Type::Boolean.new.coerce(0) 35 | # # => false 36 | # 37 | # @raise [InvalidValueError] if value is not included in true and false arrays 38 | # 39 | # @return [boolean] 40 | # 41 | # @since 0.1.0 42 | def coerce(value, _options = {}) 43 | if TRUE_VALUES.include?(value) 44 | true 45 | elsif FALSE_VALUES.include?(value) 46 | false 47 | else 48 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "Boolean") 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/date.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # Abstract class for typecast object to Date type. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Date 9 | # Convert value to Date type 10 | # 11 | # @private 12 | # 13 | # @param [Object] value 14 | # @param [Hash] _options 15 | # 16 | # @example Convert string to Date value 17 | # ShallowAttributes::Type::Date.new.coerce('Thu Nov 29 2001') 18 | # # => # 19 | # 20 | # @raise [InvalidValueError] if value is not a string 21 | # 22 | # @return [Date] 23 | # 24 | # @since 0.1.0 25 | def coerce(value, options = {}) 26 | case value 27 | when ::DateTime, ::Time then value.to_date 28 | when ::Date then value 29 | else 30 | # TODO: ::Date.parse(Class.new.to_s) valid call and will create strange Data object 31 | ::Date.parse(value.to_s) 32 | end 33 | rescue 34 | if options.fetch(:strict, true) 35 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "Date") 36 | else 37 | nil 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/date_time.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | module ShallowAttributes 4 | module Type 5 | # Abstract class for typecast object to DateTime type. 6 | # 7 | # @abstract 8 | # 9 | # @since 0.1.0 10 | class DateTime 11 | # Convert value to DateTime type 12 | # 13 | # @private 14 | # 15 | # @param [Object] value 16 | # @param [Hash] _options 17 | # 18 | # @example Convert integer to datetime value 19 | # ShallowAttributes::Type::DateTime.new.coerce('Thu Nov 29 14:33:20 GMT 2001') 20 | # # => '2001-11-29T14:33:20+00:00' 21 | # 22 | # @raise [InvalidValueError] if value is not a string 23 | # 24 | # @return [DateTime] 25 | # 26 | # @since 0.1.0 27 | def coerce(value, options = {}) 28 | case value 29 | when ::DateTime then value 30 | when ::Time then ::DateTime.parse(value.to_s) 31 | else 32 | ::DateTime.parse(value) 33 | end 34 | rescue 35 | if options.fetch(:strict, true) 36 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "DateTime") 37 | else 38 | nil 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/float.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # This class is needed to change object type to Float. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Float 9 | # Convert value to Float type 10 | # 11 | # @private 12 | # 13 | # @param [Object] value 14 | # @param [Hash] options 15 | # @option options [boolean] :allow_nil cast `nil` to integer or float 16 | # 17 | # @example Convert string to float value 18 | # ShallowAttributes::Type::Float.new.coerce('2001') 19 | # # => 2001.0 20 | # 21 | # @raise [InvalidValueError] if value is invalid 22 | # 23 | # @return [Float] 24 | # 25 | # @since 0.1.0 26 | def coerce(value, options = {}) 27 | case value 28 | when nil then options[:allow_nil] ? nil : 0.0 29 | when ::TrueClass then 1.0 30 | when ::FalseClass then 0.0 31 | else 32 | value.respond_to?(:to_f) ? value.to_f 33 | : raise(ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "Float")) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/integer.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # This class changes object to Integer. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Integer 9 | # Convert value to Integer type 10 | # 11 | # @private 12 | # 13 | # @param [Object] value 14 | # @param [Hash] options 15 | # @option options [boolean] :allow_nil cast `nil` to integer or float 16 | # 17 | # @example Convert sting to integer value 18 | # ShallowAttributes::Type::Integer.new.coerce('2001') 19 | # # => 2001 20 | # 21 | # @raise [InvalidValueError] if value is invalid 22 | # 23 | # @return [Integer] 24 | # 25 | # @since 0.1.0 26 | def coerce(value, options = {}) 27 | case value 28 | when nil then options[:allow_nil] ? nil : 0 29 | when ::TrueClass then 1 30 | when ::FalseClass then 0 31 | else 32 | value.respond_to?(:to_i) ? value.to_i 33 | : raise(ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "Integer")) 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/string.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # Abstract class for typecast object to String type. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class String 9 | # Convert value to String type 10 | # 11 | # @private 12 | # 13 | # @param [Object] value 14 | # @param [Hash] _options 15 | # 16 | # @example Convert integer to string value 17 | # ShallowAttributes::Type::String.new.coerce(2001) 18 | # # => '2001' 19 | # 20 | # @return [Sting] 21 | # 22 | # @since 0.1.0 23 | def coerce(value, options = {}) 24 | case value 25 | when nil then options[:allow_nil] ? nil : '' 26 | when ::Array then value.join 27 | when ::Hash, ::Class then error(value) 28 | else 29 | value.respond_to?(:to_s) ? value.to_s : error(value) 30 | end 31 | end 32 | 33 | private 34 | 35 | def error(value) 36 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "String") 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/shallow_attributes/type/time.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | module Type 3 | # Abstract class for typecast object to Time type. 4 | # 5 | # @abstract 6 | # 7 | # @since 0.1.0 8 | class Time 9 | # Convert value to Time type 10 | # 11 | # @private 12 | # 13 | # @param [Object] value 14 | # @param [Hash] _options 15 | # 16 | # @example Convert string to Time value 17 | # ShallowAttributes::Type::Time.new.coerce('Thu Nov 29 14:33:20 GMT 2001') 18 | # # => '2001-11-29 14:33:20 +0000' 19 | # 20 | # @raise [InvalidValueError] if value is not a sting or an integer 21 | # 22 | # @return [Time] 23 | # 24 | # @since 0.1.0 25 | def coerce(value, options = {}) 26 | case value 27 | when ::Time then value 28 | when ::Integer then ::Time.at(value) 29 | else 30 | ::Time.parse(value.to_s) 31 | end 32 | rescue 33 | if options.fetch(:strict, true) 34 | raise ShallowAttributes::Type::InvalidValueError, %(Invalid value "#{value}" for type "Time") 35 | else 36 | nil 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/shallow_attributes/version.rb: -------------------------------------------------------------------------------- 1 | module ShallowAttributes 2 | # Defines the full version 3 | # 4 | # @since 0.1.0 5 | VERSION = "0.9.5".freeze 6 | end 7 | -------------------------------------------------------------------------------- /shallow_attributes.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'shallow_attributes/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "shallow_attributes" 8 | spec.version = ShallowAttributes::VERSION 9 | spec.authors = ["Anton Davydov"] 10 | spec.email = ["antondavydov.o@gmail.com"] 11 | 12 | spec.summary = %q{Attributes for Plain Old Ruby Objects} 13 | spec.description = %q{Attributes for Plain Old Ruby Objects} 14 | spec.homepage = "https://github.com/davydovanton/shallow_attributes" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", "~> 1.11" 23 | spec.add_development_dependency "rake", "~> 10.0" 24 | spec.add_development_dependency "minitest", "~> 5.0" 25 | spec.add_development_dependency "dry-types", "~> 0.11" 26 | end 27 | -------------------------------------------------------------------------------- /test/array_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe ShallowAttributes::Type::Array do 4 | describe '#coerce' do 5 | let(:type) { ShallowAttributes::Type::Array.new } 6 | 7 | describe 'when value is not Array' do 8 | it 'returns InvalidValueError' do 9 | err = -> { type.coerce('test', of: Integer) }.must_raise ShallowAttributes::Type::InvalidValueError 10 | err.message.must_equal %(Invalid value "test" for type "Array") 11 | 12 | err = -> { type.coerce(123, of: Integer) }.must_raise ShallowAttributes::Type::InvalidValueError 13 | err.message.must_equal %(Invalid value "123" for type "Array") 14 | 15 | err = -> { type.coerce(true, of: Integer) }.must_raise ShallowAttributes::Type::InvalidValueError 16 | err.message.must_equal %(Invalid value "true" for type "Array") 17 | 18 | err = -> { type.coerce({a: :b}, of: Integer) }.must_raise ShallowAttributes::Type::InvalidValueError 19 | err.message.must_equal %(Invalid value "{:a=>:b}" for type "Array") 20 | 21 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 22 | err.message.must_equal %(Invalid value "Class" for type "Array") 23 | 24 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 25 | err.message.must_match 'Invalid value "# { type.coerce('test') }.must_raise ShallowAttributes::Type::InvalidValueError 10 | err.message.must_equal %(Invalid value "test" for type "Boolean") 11 | 12 | err = -> { type.coerce('') }.must_raise ShallowAttributes::Type::InvalidValueError 13 | err.message.must_equal %(Invalid value "" for type "Boolean") 14 | end 15 | end 16 | 17 | describe 'when value is Numeric' do 18 | it 'returns true for not zero' do 19 | type.coerce(1).must_equal true 20 | type.coerce(1.0).must_equal true 21 | end 22 | 23 | it 'returns false for zero' do 24 | type.coerce(0).must_equal false 25 | type.coerce(0.0).must_equal false 26 | end 27 | end 28 | 29 | describe 'when value is Nil' do 30 | it 'returns false' do 31 | type.coerce(nil).must_equal false 32 | end 33 | end 34 | 35 | describe 'when value is TrueClass' do 36 | it 'returns true' do 37 | type.coerce(true).must_equal true 38 | end 39 | end 40 | 41 | describe 'when value is FalseClass' do 42 | it 'returns false' do 43 | type.coerce(false).must_equal false 44 | end 45 | end 46 | 47 | describe 'when value is not allowed' do 48 | it 'returns error' do 49 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 50 | err.message.must_equal %(Invalid value "[]" for type "Boolean") 51 | 52 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 53 | err.message.must_equal %(Invalid value "{}" for type "Boolean") 54 | 55 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 56 | err.message.must_equal %(Invalid value "1" for type "Boolean") 57 | 58 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 59 | err.message.must_equal %(Invalid value "Class" for type "Boolean") 60 | 61 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 62 | err.message.must_match 'Invalid value "# { type.coerce('') }.must_raise ShallowAttributes::Type::InvalidValueError 30 | err.message.must_equal %(Invalid value "" for type "DateTime") 31 | 32 | err = -> { type.coerce('1') }.must_raise ShallowAttributes::Type::InvalidValueError 33 | err.message.must_equal %(Invalid value "1" for type "DateTime") 34 | end 35 | end 36 | 37 | describe 'when value is Numeric' do 38 | it 'returns InvalidValueError' do 39 | err = -> { type.coerce(123123123) }.must_raise ShallowAttributes::Type::InvalidValueError 40 | err.message.must_equal %(Invalid value "123123123" for type "DateTime") 41 | end 42 | end 43 | 44 | describe 'when value is Nil' do 45 | it 'returns InvalidValueError' do 46 | err = -> { type.coerce(nil) }.must_raise ShallowAttributes::Type::InvalidValueError 47 | err.message.must_equal %(Invalid value "" for type "DateTime") 48 | end 49 | end 50 | 51 | describe 'when strict is false' do 52 | it 'returns nil' do 53 | assert_nil type.coerce(nil, strict: false) 54 | end 55 | end 56 | 57 | describe 'when value is TrueClass' do 58 | it 'returns InvalidValueError' do 59 | err = -> { type.coerce(true) }.must_raise ShallowAttributes::Type::InvalidValueError 60 | err.message.must_equal %(Invalid value "true" for type "DateTime") 61 | end 62 | end 63 | 64 | describe 'when value is FalseClass' do 65 | it 'returns InvalidValueError' do 66 | err = -> { type.coerce(false) }.must_raise ShallowAttributes::Type::InvalidValueError 67 | err.message.must_equal %(Invalid value "false" for type "DateTime") 68 | end 69 | end 70 | 71 | describe 'when value is not allowed' do 72 | it 'returns error' do 73 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 74 | err.message.must_equal %(Invalid value "[]" for type "DateTime") 75 | 76 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 77 | err.message.must_equal %(Invalid value "{}" for type "DateTime") 78 | 79 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 80 | err.message.must_equal %(Invalid value "1" for type "DateTime") 81 | 82 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 83 | err.message.must_equal %(Invalid value "Class" for type "DateTime") 84 | 85 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 86 | err.message.must_match 'Invalid value "# { type.coerce('') }.must_raise ShallowAttributes::Type::InvalidValueError 37 | err.message.must_equal %(Invalid value "" for type "Date") 38 | 39 | err = -> { type.coerce('asd') }.must_raise ShallowAttributes::Type::InvalidValueError 40 | err.message.must_equal %(Invalid value "asd" for type "Date") 41 | 42 | err = -> { type.coerce('123123') }.must_raise ShallowAttributes::Type::InvalidValueError 43 | err.message.must_equal %(Invalid value "123123" for type "Date") 44 | end 45 | end 46 | 47 | describe 'when value is Nil' do 48 | it 'returns InvalidValueError' do 49 | err = -> { type.coerce(nil) }.must_raise ShallowAttributes::Type::InvalidValueError 50 | err.message.must_equal %(Invalid value "" for type "Date") 51 | end 52 | end 53 | 54 | describe 'when value is TrueClass' do 55 | it 'returns InvalidValueError' do 56 | err = -> { type.coerce(true) }.must_raise ShallowAttributes::Type::InvalidValueError 57 | err.message.must_equal %(Invalid value "true" for type "Date") 58 | end 59 | end 60 | 61 | describe 'when value is FalseClass' do 62 | it 'returns InvalidValueError' do 63 | err = -> { type.coerce(false) }.must_raise ShallowAttributes::Type::InvalidValueError 64 | err.message.must_equal %(Invalid value "false" for type "Date") 65 | end 66 | end 67 | 68 | describe 'when value is not allowed' do 69 | it 'returns error' do 70 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 71 | err.message.must_equal %(Invalid value "[]" for type "Date") 72 | 73 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 74 | err.message.must_equal %(Invalid value "{}" for type "Date") 75 | 76 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 77 | err.message.must_equal %(Invalid value "1" for type "Date") 78 | 79 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 80 | err.message.must_equal %(Invalid value "Class" for type "Date") 81 | 82 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 83 | err.message.must_match 'Invalid value "# 'Anton', 'age' => 22) 54 | 55 | user.name.must_equal 'Anton' 56 | user.age.must_equal 22 57 | end 58 | 59 | it 'does not mutate the argument' do 60 | params = {'name' => 'Anton', 'age' => 22} 61 | MainDryUser.new(params) 62 | params.must_equal({'name' => 'Anton', 'age' => 22}) 63 | end 64 | end 65 | 66 | describe '#attributes=' do 67 | it 'mass-assigns attributes from symbolized hash' do 68 | user.attributes = { name: 'Alex' } 69 | 70 | user.name.must_equal 'Alex' 71 | user.age.must_equal 22 72 | end 73 | 74 | it 'mass-assigns attributes from stringified hash' do 75 | user.attributes = { 'name' => 'Alex' } 76 | 77 | user.name.must_equal 'Alex' 78 | user.age.must_equal 22 79 | end 80 | 81 | it 'mass-assigns uninitialized attributes from stringified hash' do 82 | user = MainDryUser.new 83 | user.age.must_be_nil 84 | 85 | user.attributes = {"age" => "21"} 86 | user.age.must_equal 21 87 | end 88 | end 89 | 90 | describe '#attributes' do 91 | it 'returns attributes like hash' do 92 | user.attributes.must_equal(name: 'Anton', age: 22) 93 | end 94 | 95 | describe 'when value is nil' do 96 | it 'returns attributes like hash' do 97 | user.name = nil 98 | user.age = 0 99 | user.attributes.must_equal(name: '', age: 0) 100 | end 101 | end 102 | end 103 | 104 | describe 'setters' do 105 | let(:user) { MainDryUser.new(name: 'Anton', age: '22', birthday: 'Thu Nov 29 14:33:20 GMT 2001') } 106 | 107 | it 'converts value to specific type' do 108 | user.age.must_equal 22 109 | user.birthday.must_be_instance_of DateTime 110 | user.birthday.to_s.must_equal '2001-11-29T14:33:20+00:00' 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test/float_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe ShallowAttributes::Type::Float do 4 | let(:type) { ShallowAttributes::Type::Float.new } 5 | 6 | describe '#coerce' do 7 | describe 'when value is String' do 8 | it 'returns float' do 9 | type.coerce('').must_equal 0.0 10 | type.coerce('1').must_equal 1.0 11 | type.coerce('1.1').must_equal 1.1 12 | end 13 | end 14 | 15 | describe 'when value is Numeric' do 16 | it 'returns float' do 17 | type.coerce(1).must_equal 1.0 18 | type.coerce(1.1).must_equal 1.1 19 | end 20 | end 21 | 22 | describe 'when value is Nil' do 23 | it 'returns nil' do 24 | type.coerce(nil).must_equal 0 25 | end 26 | end 27 | 28 | describe 'when allow_nil is true' do 29 | it 'returns float' do 30 | assert_nil type.coerce(nil, allow_nil: true) 31 | end 32 | end 33 | 34 | describe 'when value is TrueClass' do 35 | it 'returns float' do 36 | type.coerce(true).must_equal 1.0 37 | end 38 | end 39 | 40 | describe 'when value is FalseClass' do 41 | it 'returns float' do 42 | type.coerce(false).must_equal 0.0 43 | end 44 | end 45 | 46 | describe 'when value is not allowed' do 47 | it 'returns error' do 48 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 49 | err.message.must_equal %(Invalid value "[]" for type "Float") 50 | 51 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 52 | err.message.must_equal %(Invalid value "{}" for type "Float") 53 | 54 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 55 | err.message.must_equal %(Invalid value "1" for type "Float") 56 | 57 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 58 | err.message.must_equal %(Invalid value "Class" for type "Float") 59 | 60 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 61 | err.message.must_match 'Invalid value "# { moderator.last_name }.must_raise NoMethodError 72 | end 73 | end 74 | 75 | describe 'for admin object' do 76 | it 'returns name attribute correct' do 77 | admin.name.must_equal 'Anton' 78 | end 79 | 80 | it 'returns role attribute correct' do 81 | admin.role.must_equal 0 82 | end 83 | 84 | it 'raises error for rates attribute' do 85 | -> { admin.rates }.must_raise NoMethodError 86 | end 87 | 88 | it 'returns last_name attribute correct' do 89 | admin.last_name.must_equal 'New' 90 | end 91 | end 92 | end 93 | 94 | describe 'with inheritance from class that mixes in ShallowAttributes, but does not define attributes' do 95 | it 'returns email attribute correct' do 96 | search = Sub1.new(email: 'foo@bar.com') 97 | search.email.must_equal 'foo@bar.com' 98 | end 99 | end 100 | 101 | describe 'with inheritance from class that does not mixin ShallowAttributes' do 102 | it 'returns email attribute correct' do 103 | search = Sub2.new(email: 'foo@bar.com') 104 | search.email.must_equal 'foo@bar.com' 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/integer_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe ShallowAttributes::Type::Integer do 4 | describe '#coerce' do 5 | let(:type) { ShallowAttributes::Type::Integer.new } 6 | 7 | describe 'when value is String' do 8 | it 'returns integer' do 9 | type.coerce('1').must_equal 1 10 | type.coerce('1.0').must_equal 1 11 | end 12 | end 13 | 14 | describe 'when value is Numeric' do 15 | it 'returns integer' do 16 | type.coerce(1).must_equal 1 17 | type.coerce(1.1).must_equal 1 18 | end 19 | end 20 | 21 | describe 'when value is Nil' do 22 | it 'returns nil' do 23 | type.coerce(nil).must_equal 0 24 | end 25 | end 26 | 27 | describe 'when allow_nil is true' do 28 | it 'returns integer' do 29 | assert_nil type.coerce(nil, allow_nil: true) 30 | end 31 | end 32 | 33 | describe 'when value is TrueClass' do 34 | it 'returns integer' do 35 | type.coerce(true).must_equal 1 36 | end 37 | end 38 | 39 | describe 'when value is FalseClass' do 40 | it 'returns integer' do 41 | type.coerce(false).must_equal 0 42 | end 43 | end 44 | 45 | describe 'when value is not allowed' do 46 | it 'returns error' do 47 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 48 | err.message.must_equal %(Invalid value "[]" for type "Integer") 49 | 50 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 51 | err.message.must_equal %(Invalid value "{}" for type "Integer") 52 | 53 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 54 | err.message.must_equal %(Invalid value "1" for type "Integer") 55 | 56 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 57 | err.message.must_equal %(Invalid value "Class" for type "Integer") 58 | 59 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 60 | err.message.must_match 'Invalid value "# { Book.new }.must_raise ShallowAttributes::MissingAttributeError 37 | err.message.must_equal 'Mandatory attribute "title" was not provided' 38 | end 39 | 40 | it 'works fine for present attribute' do 41 | Book.new(title: 'The Stranger').title.must_equal 'The Stranger' 42 | end 43 | end 44 | 45 | describe 'for more than one attribute' do 46 | it 'raises error for the first attribute' do 47 | err = -> { CreditCard.new }.must_raise ShallowAttributes::MissingAttributeError 48 | err.message.must_equal 'Mandatory attribute "number" was not provided' 49 | end 50 | 51 | it 'works fine for present attributes' do 52 | CreditCard.new(number: 123, owner: 'Andrew').number.must_equal 123 53 | end 54 | end 55 | 56 | describe 'for an attribute with a default value' do 57 | # I don't know for what reason someone would set presence & default options 58 | # together, but just in case 59 | 60 | it 'sets missing attribute to default value' do 61 | FridgeWithDefault.new.temperature.must_equal '+4C' 62 | end 63 | end 64 | 65 | describe 'for various types' do 66 | let(:options) { Hash[datetime: DateTime.now, float: 0.10, time: DateTime.now, bool: true] } 67 | 68 | it 'sets default array value to [], so nothing is raised' do 69 | TypeChecking.new(options).arr.must_equal [] 70 | end 71 | 72 | it 'raises error if DateTime is not present' do 73 | -> { TypeChecking.new(options.tap { |opts| opts.delete(:datetime) }) } 74 | .must_raise ShallowAttributes::MissingAttributeError 75 | end 76 | 77 | it 'raises error if Float is not present' do 78 | -> { TypeChecking.new(options.tap { |opts| opts.delete(:float) }) } 79 | .must_raise ShallowAttributes::MissingAttributeError 80 | end 81 | 82 | it 'raises error if Time is not present' do 83 | -> { TypeChecking.new(options.tap { |opts| opts.delete(:time) }) } 84 | .must_raise ShallowAttributes::MissingAttributeError 85 | end 86 | 87 | it 'raises error if Boolean is not present' do 88 | -> { TypeChecking.new(options.tap { |opts| opts.delete(:bool) }) } 89 | .must_raise ShallowAttributes::MissingAttributeError 90 | end 91 | end 92 | end 93 | 94 | describe 'with present option set to false' do 95 | # Again, I doubt that anyone would ever specify 'present: false', but just in case 96 | it 'does not raise error' do 97 | UserWithPresentFalse.new(name: 'Nikita').name.must_equal 'Nikita' 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/shallow_attributes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SimpleUser 4 | include ShallowAttributes 5 | attribute :name, String, defauil: 'Ben' 6 | end 7 | 8 | class MainUser 9 | include ShallowAttributes 10 | 11 | attribute :name, String, default: 'Ben' 12 | attribute :last_name, String, default: :default_last_name 13 | attribute :full_name, String, default: -> (user, attribute) { "#{user.name} #{user.last_name}" } 14 | attribute :age, Integer 15 | attribute :birthday, DateTime 16 | attribute :color, String, default: :default_color 17 | 18 | attribute :friends_count, Integer, default: 0 19 | attribute :sizes, Array, of: Integer 20 | 21 | attribute :admin, Boolean, default: false 22 | 23 | def default_last_name 24 | 'Affleck' 25 | end 26 | 27 | private 28 | 29 | def default_color 30 | 'Pink' 31 | end 32 | end 33 | 34 | describe ShallowAttributes do 35 | let(:user) { MainUser.new(name: 'Anton', age: 22) } 36 | 37 | describe '::attributes' do 38 | it 'returns class attributes array' do 39 | MainUser.attributes.must_equal(%i(name last_name full_name age birthday color friends_count sizes admin)) 40 | end 41 | end 42 | 43 | describe 'on initialize' do 44 | let(:user) { MainUser.new } 45 | 46 | it 'builds getters for each attribute' do 47 | assert_nil user.age 48 | assert_nil user.birthday 49 | user.sizes.must_equal [] 50 | end 51 | 52 | it 'builds setter for attribute' do 53 | user.name = 'Anton' 54 | user.name.must_equal 'Anton' 55 | 56 | user.age = 22 57 | user.age.must_equal 22 58 | end 59 | 60 | it 'sets attributes from symbolized hash' do 61 | user = MainUser.new(name: 'Anton', age: 22) 62 | 63 | user.name.must_equal 'Anton' 64 | user.age.must_equal 22 65 | end 66 | 67 | it 'sets attributes from stringified hash' do 68 | user = MainUser.new('name' => 'Anton', 'age' => 22) 69 | 70 | user.name.must_equal 'Anton' 71 | user.age.must_equal 22 72 | end 73 | 74 | it 'does not mutate the argument' do 75 | params = {'name' => 'Anton', 'age' => 22} 76 | MainUser.new(params) 77 | params.must_equal({'name' => 'Anton', 'age' => 22}) 78 | end 79 | 80 | it 'sets object as default value for each attribute' do 81 | user.name.must_equal 'Ben' 82 | user.friends_count.must_equal 0 83 | user.admin.must_equal false 84 | end 85 | 86 | it 'sets method name as default value for each attribute' do 87 | user.last_name.must_equal 'Affleck' 88 | end 89 | 90 | it 'sets lambda as default value for each attribute' do 91 | user.full_name.must_equal 'Ben Affleck' 92 | end 93 | 94 | describe 'with array type' do 95 | it 'initializes array of objects' do 96 | user = MainUser.new(sizes: %w[1 2 3]) 97 | user.sizes.must_equal [1, 2, 3] 98 | end 99 | 100 | it 'builds setter for array attribute' do 101 | user = MainUser.new 102 | user.sizes = %w[1 2 3] 103 | 104 | user.sizes.must_equal [1, 2, 3] 105 | end 106 | end 107 | end 108 | 109 | describe '#attributes=' do 110 | it 'mass-assigns attributes from symbolized hash' do 111 | user.attributes = { name: 'Alex' } 112 | 113 | user.name.must_equal 'Alex' 114 | user.age.must_equal 22 115 | end 116 | 117 | it 'mass-assigns attributes from stringified hash' do 118 | user.attributes = { 'name' => 'Alex' } 119 | 120 | user.name.must_equal 'Alex' 121 | user.age.must_equal 22 122 | end 123 | 124 | it 'mass-assigns uninitialized attributes from stringified hash' do 125 | user = MainUser.new 126 | user.age.must_be_nil 127 | 128 | user.attributes = {"age" => "21"} 129 | user.age.must_equal 21 130 | end 131 | end 132 | 133 | describe '#attributes' do 134 | it 'returns attributes like hash' do 135 | user.attributes.must_equal(name: 'Anton', age: 22, last_name: "Affleck", full_name: "Anton Affleck", color: "Pink", friends_count: 0, sizes: [], admin: false) 136 | end 137 | 138 | describe 'when value is nil' do 139 | it 'returns attributes like hash' do 140 | user.name = nil 141 | user.age = nil 142 | user.admin = nil 143 | user.attributes.must_equal(name: '', age: 0, last_name: "Affleck", full_name: "Anton Affleck", color: "Pink", friends_count: 0, sizes: [], admin: false) 144 | end 145 | end 146 | end 147 | 148 | describe '#reset_attribute' do 149 | it 'reserts attribute value attributes like hash' do 150 | user.reset_attribute(:name) 151 | user.name.must_equal 'Ben' 152 | end 153 | end 154 | 155 | describe '#==' do 156 | it 'returns true if objects are equal' do 157 | user1 = MainUser.new 158 | user2 = MainUser.new 159 | user1.must_equal user2 160 | end 161 | 162 | it 'returns false if objects are not equal' do 163 | user1 = MainUser.new(name: 'Anton') 164 | user2 = MainUser.new(name: 'Jon') 165 | user1.wont_equal user2 166 | end 167 | end 168 | 169 | describe '#inspect' do 170 | it 'returns string with object information' do 171 | user = SimpleUser.new(name: 'Anton') 172 | user.inspect.must_equal "#" 173 | end 174 | end 175 | 176 | describe 'setters' do 177 | let(:user) { MainUser.new(name: 'Anton', age: '22', birthday: 'Thu Nov 29 14:33:20 GMT 2001') } 178 | 179 | it 'converts value to specific type' do 180 | user.age.must_equal 22 181 | user.birthday.must_be_instance_of DateTime 182 | user.birthday.to_s.must_equal '2001-11-29T14:33:20+00:00' 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/string_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe ShallowAttributes::Type::String do 4 | let(:type) { ShallowAttributes::Type::String.new } 5 | 6 | describe '#coerce' do 7 | describe 'when value is String' do 8 | it 'returns string' do 9 | type.coerce('').must_equal '' 10 | type.coerce('test').must_equal 'test' 11 | end 12 | end 13 | 14 | describe 'when value is Symbol' do 15 | it 'returns string' do 16 | type.coerce(:test).must_equal 'test' 17 | end 18 | end 19 | 20 | describe 'when value is Numeric' do 21 | it 'returns string' do 22 | type.coerce(1).must_equal '1' 23 | type.coerce(1.1).must_equal '1.1' 24 | end 25 | end 26 | 27 | describe 'when value is Nil' do 28 | it 'returns empty string' do 29 | type.coerce(nil).must_equal '' 30 | end 31 | end 32 | 33 | describe 'when allow_nil is true' do 34 | it 'returns integer' do 35 | assert_nil type.coerce(nil, allow_nil: true) 36 | end 37 | end 38 | 39 | describe 'when value is TrueClass' do 40 | it 'returns string' do 41 | type.coerce(true).must_equal 'true' 42 | end 43 | end 44 | 45 | describe 'when value is FalseClass' do 46 | it 'returns string' do 47 | type.coerce(false).must_equal 'false' 48 | end 49 | end 50 | 51 | describe 'when value is Array' do 52 | it 'returns string' do 53 | type.coerce([]).must_equal '' 54 | type.coerce([1, 2, 'string']).must_equal '12string' 55 | end 56 | end 57 | 58 | describe 'when value is Hash' do 59 | it 'returns error' do 60 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 61 | err.message.must_equal %(Invalid value "{}" for type "String") 62 | end 63 | end 64 | 65 | describe 'when value is Class' do 66 | it 'returns error' do 67 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 68 | err.message.must_equal %(Invalid value "Class" for type "String") 69 | end 70 | end 71 | 72 | describe 'when value is custom object with #to_s' do 73 | class Test 74 | def to_s 75 | 'hello' 76 | end 77 | end 78 | 79 | it 'returns error' do 80 | type.coerce(Test.new).must_equal 'hello' 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'shallow_attributes' 6 | 7 | require 'minitest/autorun' 8 | -------------------------------------------------------------------------------- /test/time_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe ShallowAttributes::Type::Time do 4 | let(:type) { ShallowAttributes::Type::Time.new } 5 | 6 | describe '#coerce' do 7 | describe 'when value is DateTime' do 8 | it 'returns time object' do 9 | time = DateTime.now 10 | type.coerce(time).class.must_equal Time 11 | end 12 | end 13 | 14 | describe 'when value is Time' do 15 | it 'returns time object' do 16 | time = Time.now 17 | type.coerce(time).must_equal time 18 | end 19 | end 20 | 21 | describe 'when value is String' do 22 | it 'returns time object' do 23 | type.coerce('Thu Nov 29 14:33:20 GMT 2001').to_s.must_equal '2001-11-29 14:33:20 +0000' 24 | end 25 | end 26 | 27 | describe 'when value is invalid String' do 28 | it 'returns error' do 29 | err = -> { type.coerce('') }.must_raise ShallowAttributes::Type::InvalidValueError 30 | err.message.must_equal %(Invalid value "" for type "Time") 31 | 32 | err = -> { type.coerce('asd') }.must_raise ShallowAttributes::Type::InvalidValueError 33 | err.message.must_equal %(Invalid value "asd" for type "Time") 34 | 35 | err = -> { type.coerce('123123') }.must_raise ShallowAttributes::Type::InvalidValueError 36 | err.message.must_equal %(Invalid value "123123" for type "Time") 37 | end 38 | end 39 | 40 | describe 'when value is Numeric' do 41 | it 'returns time object' do 42 | type.coerce(628232400).to_s.must_equal Time.at(628232400).to_s 43 | end 44 | end 45 | 46 | describe 'when value is Nil' do 47 | it 'returns InvalidValueError' do 48 | err = -> { type.coerce(nil) }.must_raise ShallowAttributes::Type::InvalidValueError 49 | err.message.must_equal %(Invalid value "" for type "Time") 50 | end 51 | end 52 | 53 | describe 'when strict is false' do 54 | it 'returns nil' do 55 | assert_nil type.coerce(nil, strict: false) 56 | end 57 | end 58 | 59 | describe 'when value is TrueClass' do 60 | it 'returns InvalidValueError' do 61 | err = -> { type.coerce(true) }.must_raise ShallowAttributes::Type::InvalidValueError 62 | err.message.must_equal %(Invalid value "true" for type "Time") 63 | end 64 | end 65 | 66 | describe 'when value is FalseClass' do 67 | it 'returns InvalidValueError' do 68 | err = -> { type.coerce(false) }.must_raise ShallowAttributes::Type::InvalidValueError 69 | err.message.must_equal %(Invalid value "false" for type "Time") 70 | end 71 | end 72 | 73 | describe 'when value is not allowed' do 74 | it 'returns error' do 75 | err = -> { type.coerce([]) }.must_raise ShallowAttributes::Type::InvalidValueError 76 | err.message.must_equal %(Invalid value "[]" for type "Time") 77 | 78 | err = -> { type.coerce({}) }.must_raise ShallowAttributes::Type::InvalidValueError 79 | err.message.must_equal %(Invalid value "{}" for type "Time") 80 | 81 | err = -> { type.coerce(:'1') }.must_raise ShallowAttributes::Type::InvalidValueError 82 | err.message.must_equal %(Invalid value "1" for type "Time") 83 | 84 | err = -> { type.coerce(Class) }.must_raise ShallowAttributes::Type::InvalidValueError 85 | err.message.must_equal %(Invalid value "Class" for type "Time") 86 | 87 | err = -> { type.coerce(Class.new) }.must_raise ShallowAttributes::Type::InvalidValueError 88 | err.message.must_match 'Invalid value "#