├── .gitignore ├── .pelusa.yml ├── .rspec ├── .ruby-gemset ├── .travis.yml ├── .yardopts ├── CONTRIBUTING.md ├── Changelog.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO.md ├── lib ├── virtus.rb └── virtus │ ├── attribute.rb │ ├── attribute │ ├── accessor.rb │ ├── boolean.rb │ ├── builder.rb │ ├── coercer.rb │ ├── coercible.rb │ ├── collection.rb │ ├── default_value.rb │ ├── default_value │ │ ├── from_callable.rb │ │ ├── from_clonable.rb │ │ └── from_symbol.rb │ ├── embedded_value.rb │ ├── hash.rb │ ├── lazy_default.rb │ ├── nullify_blank.rb │ └── strict.rb │ ├── attribute_set.rb │ ├── builder.rb │ ├── builder │ └── hook_context.rb │ ├── class_inclusions.rb │ ├── class_methods.rb │ ├── coercer.rb │ ├── configuration.rb │ ├── const_missing_extensions.rb │ ├── extensions.rb │ ├── instance_methods.rb │ ├── model.rb │ ├── module_extensions.rb │ ├── support │ ├── equalizer.rb │ ├── options.rb │ └── type_lookup.rb │ ├── value_object.rb │ └── version.rb ├── spec ├── integration │ ├── attributes_attribute_spec.rb │ ├── building_module_spec.rb │ ├── collection_member_coercion_spec.rb │ ├── custom_attributes_spec.rb │ ├── custom_collection_attributes_spec.rb │ ├── default_values_spec.rb │ ├── defining_attributes_spec.rb │ ├── embedded_value_spec.rb │ ├── extending_objects_spec.rb │ ├── hash_attributes_coercion_spec.rb │ ├── inheritance_spec.rb │ ├── injectible_coercers_spec.rb │ ├── mass_assignment_with_accessors_spec.rb │ ├── overriding_virtus_spec.rb │ ├── required_attributes_spec.rb │ ├── struct_as_embedded_value_spec.rb │ ├── using_modules_spec.rb │ ├── value_object_with_custom_constructor_spec.rb │ └── virtus │ │ ├── instance_level_attributes_spec.rb │ │ └── value_object_spec.rb ├── shared │ ├── constants_helpers.rb │ ├── freeze_method_behavior.rb │ ├── idempotent_method_behaviour.rb │ └── options_class_method.rb ├── spec_helper.rb └── unit │ └── virtus │ ├── attribute │ ├── boolean │ │ ├── coerce_spec.rb │ │ └── value_coerced_predicate_spec.rb │ ├── class_methods │ │ ├── build_spec.rb │ │ └── coerce_spec.rb │ ├── coerce_spec.rb │ ├── coercible_predicate_spec.rb │ ├── collection │ │ ├── class_methods │ │ │ └── build_spec.rb │ │ ├── coerce_spec.rb │ │ └── value_coerced_predicate_spec.rb │ ├── comparison_spec.rb │ ├── custom_collection_spec.rb │ ├── defined_spec.rb │ ├── embedded_value │ │ ├── class_methods │ │ │ └── build_spec.rb │ │ └── coerce_spec.rb │ ├── get_spec.rb │ ├── hash │ │ ├── class_methods │ │ │ └── build_spec.rb │ │ └── coerce_spec.rb │ ├── lazy_predicate_spec.rb │ ├── rename_spec.rb │ ├── required_predicate_spec.rb │ ├── set_default_value_spec.rb │ ├── set_spec.rb │ └── value_coerced_predicate_spec.rb │ ├── attribute_set │ ├── append_spec.rb │ ├── define_reader_method_spec.rb │ ├── define_writer_method_spec.rb │ ├── each_spec.rb │ ├── element_reference_spec.rb │ ├── element_set_spec.rb │ ├── merge_spec.rb │ └── reset_spec.rb │ ├── attribute_spec.rb │ ├── attributes_reader_spec.rb │ ├── attributes_writer_spec.rb │ ├── class_methods │ ├── finalize_spec.rb │ └── new_spec.rb │ ├── config_spec.rb │ ├── element_reader_spec.rb │ ├── element_writer_spec.rb │ ├── freeze_spec.rb │ ├── model_spec.rb │ ├── module_spec.rb │ ├── set_default_attributes_spec.rb │ └── value_object_spec.rb └── virtus.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## Rubinius 17 | *.rbc 18 | .rbx 19 | 20 | ## PROJECT::GENERAL 21 | *.gem 22 | coverage 23 | profiling 24 | turbulence 25 | rdoc 26 | pkg 27 | tmp 28 | doc 29 | log 30 | .yardoc 31 | measurements 32 | 33 | ## BUNDLER 34 | .bundle 35 | Gemfile.lock 36 | bin/ 37 | 38 | ## PROJECT::SPECIFIC 39 | -------------------------------------------------------------------------------- /.pelusa.yml: -------------------------------------------------------------------------------- 1 | sources: lib/**/*.rb 2 | 3 | lints: 4 | InstanceVariables: 5 | limit: 3 6 | LineRestriction: 7 | limit: 124 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | virtus 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | bundler_args: --without tools 4 | cache: bundler 5 | rvm: 6 | - 2.0 7 | - 2.1 8 | - 2.2 9 | - 2.3 10 | - 2.4 11 | - 2.5 12 | - 2.6 13 | - 2.7 14 | - 3.0 15 | - jruby 16 | before_script: 17 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 18 | - chmod +x ./cc-test-reporter 19 | - ./cc-test-reporter before-build 20 | after_script: 21 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 22 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | - README.md History.md LICENSE 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Current project status 2 | 3 | Virtus recently hit it's 1.0 release (2013-10-16). The focus now is on bug-fixes and maintenance while [@solnic][solnic] is away from the project. An experimental branch will be kept up-to date where proposed features and API changes can be made. Please direct your questions and issues to [@elskwid][elskwid], the maintainer. 4 | 5 | # Contributing to Virtus 6 | 7 | * Fork the project. 8 | * Make your feature addition or bug fix. 9 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 10 | * Commit, do not mess with Rakefile or version 11 | (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) 12 | * Send me a pull request. Bonus points for topic branches. 13 | 14 | Author: [@solnic][solnic] 15 | Maintainer: [@elskwid][elskwid] 16 | 17 | [solnic]: https://github.com/solnic 18 | [elskwid]: https://github.com/elskwid 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'dry-inflector' 6 | gem 'rspec' 7 | gem 'bogus' 8 | gem 'simplecov', platform: :ruby 9 | 10 | gem "codeclimate-test-reporter", group: :test, require: false 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, spec_paths: 'spec/unit' do 2 | #run all specs if configuration is modified 3 | watch('Guardfile') { 'spec' } 4 | watch('Gemfile.lock') { 'spec' } 5 | watch('spec/spec_helper.rb') { 'spec' } 6 | 7 | # run all specs if supporting files files are modified 8 | watch(%r{\Aspec/(?:lib|support|shared)/.+\.rb\z}) { 'spec' } 9 | 10 | # run unit specs if associated lib code is modified 11 | watch(%r{\Alib/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}"] } 12 | watch(%r{\Alib/(.+)/support/(.+)\.rb\z}) { |m| Dir["spec/unit/#{m[1]}/#{m[2]}"] } 13 | watch("lib/#{File.basename(File.expand_path('../', __FILE__))}.rb") { 'spec' } 14 | 15 | # run a spec if it is modified 16 | watch(%r{\Aspec/(?:unit|integration)/.+_spec\.rb\z}) 17 | 18 | notification :tmux, :display_message => true 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Piotr Solnica 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rspec/core/rake_task" 2 | 3 | RSpec::Core::RakeTask.new(:spec) 4 | task default: [:spec] 5 | 6 | begin 7 | require "rubocop/rake_task" 8 | 9 | Rake::Task[:default].enhance [:rubocop] 10 | 11 | RuboCop::RakeTask.new do |task| 12 | task.options << "--display-cop-names" 13 | end 14 | rescue LoadError 15 | end 16 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 1.0.0.rc ROADMAP: 2 | 3 | - Refactor to use soon-to-be-extracted axiom-types 4 | - Add ability to pass in custom reader/writer *objects* 5 | - Refactor to use extracted equalizer gem 6 | - Tune Adamantium usage to memoize common methods in Attribute 7 | -------------------------------------------------------------------------------- /lib/virtus.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | # Base module which adds Attribute API to your classes 4 | module Virtus 5 | 6 | # Provides args for const_get and const_defined? to make them behave 7 | # consistently across different versions of ruby 8 | EXTRA_CONST_ARGS = (RUBY_VERSION < '1.9' ? [] : [ false ]).freeze 9 | 10 | # Represents an undefined parameter used by auto-generated option methods 11 | Undefined = Object.new.freeze 12 | 13 | class CoercionError < StandardError 14 | attr_reader :output, :attribute 15 | 16 | def initialize(output, attribute) 17 | @output, @attribute = output, attribute 18 | super(build_message) 19 | end 20 | 21 | def build_message 22 | if attribute_name? 23 | "Failed to coerce attribute `#{attribute_name}' from #{output.inspect} into #{target_type}" 24 | else 25 | "Failed to coerce #{output.inspect} into #{target_type}" 26 | end 27 | end 28 | 29 | def attribute_name 30 | attribute.options[:name] 31 | end 32 | 33 | def attribute_name? 34 | attribute_name ? true : false 35 | end 36 | 37 | def target_type 38 | attribute.primitive.inspect 39 | end 40 | end 41 | 42 | # Extends base class or a module with virtus methods 43 | # 44 | # @param [Object] object 45 | # 46 | # @return [undefined] 47 | # 48 | # @deprecated 49 | # 50 | # @api private 51 | def self.included(object) 52 | super 53 | if Class === object 54 | Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.model' instead #{caller.first}") 55 | object.send(:include, ClassInclusions) 56 | else 57 | Virtus.warn("including Virtus module is deprecated. Use 'include Virtus.module' instead #{caller.first}") 58 | object.extend(ModuleExtensions) 59 | end 60 | end 61 | private_class_method :included 62 | 63 | # Extends an object with virtus extensions 64 | # 65 | # @param [Object] object 66 | # 67 | # @return [undefined] 68 | # 69 | # @deprecated 70 | # 71 | # @api private 72 | def self.extended(object) 73 | Virtus.warn("extending with Virtus module is deprecated. Use 'extend(Virtus.model)' instead #{caller.first}") 74 | object.extend(Extensions) 75 | end 76 | private_class_method :extended 77 | 78 | # Sets the global coercer configuration 79 | # 80 | # @example 81 | # Virtus.coercer do |config| 82 | # config.string.boolean_map = { true => '1', false => '0' } 83 | # end 84 | # 85 | # @return [Coercible::Coercer] 86 | # 87 | # @api public 88 | def self.coercer(&block) 89 | configuration.coercer(&block) 90 | end 91 | 92 | # Sets the global coercion configuration value 93 | # 94 | # @param [Boolean] value 95 | # 96 | # @return [Virtus] 97 | # 98 | # @api public 99 | def self.coerce=(value) 100 | configuration.coerce = value 101 | self 102 | end 103 | 104 | # Returns the global coercion setting 105 | # 106 | # @return [Boolean] 107 | # 108 | # @api public 109 | def self.coerce 110 | configuration.coerce 111 | end 112 | 113 | # Provides access to the global Virtus configuration 114 | # 115 | # @example 116 | # Virtus.config do |config| 117 | # config.coerce = false 118 | # end 119 | # 120 | # @return [Configuration] 121 | # 122 | # @api public 123 | def self.config(&block) 124 | yield configuration if block_given? 125 | configuration 126 | end 127 | 128 | # Provides access to the Virtus module builder 129 | # see Virtus::ModuleBuilder 130 | # 131 | # @example 132 | # MyVirtusModule = Virtus.module { |mod| 133 | # mod.coerce = true 134 | # mod.string.boolean_map = { 'yup' => true, 'nope' => false } 135 | # } 136 | # 137 | # class Book 138 | # include MyVirtusModule 139 | # 140 | # attribute :published, Boolean 141 | # end 142 | # 143 | # # This could be made more succinct as well 144 | # class OtherBook 145 | # include Virtus.module { |m| m.coerce = false } 146 | # end 147 | # 148 | # @return [Module] 149 | # 150 | # @api public 151 | def self.model(options = {}, &block) 152 | ModelBuilder.call(options, &block) 153 | end 154 | 155 | # Builds a module for...modules 156 | # 157 | # @example 158 | # 159 | # module Common 160 | # include Virtus.module 161 | # 162 | # attribute :name, String 163 | # attribute :age, Integer 164 | # end 165 | # 166 | # class User 167 | # include Common 168 | # end 169 | # 170 | # class Admin 171 | # include Common 172 | # end 173 | # 174 | # @return [Module] 175 | # 176 | # @api public 177 | def self.module(options = {}, &block) 178 | ModuleBuilder.call(options, &block) 179 | end 180 | 181 | # Builds a module for value object models 182 | # 183 | # @example 184 | # 185 | # class GeoLocation 186 | # include Virtus.value_object 187 | # 188 | # values do 189 | # attribute :lat, Float 190 | # attribute :lng, Float 191 | # end 192 | # end 193 | # 194 | # @return [Module] 195 | # 196 | # @api public 197 | def self.value_object(options = {}, &block) 198 | ValueObjectBuilder.call(options, &block) 199 | end 200 | 201 | # Global configuration instance 202 | # 203 | # @ return [Configuration] 204 | # 205 | # @api private 206 | def self.configuration 207 | @configuration ||= Configuration.new 208 | end 209 | 210 | # @api private 211 | def self.constantize(type) 212 | inflector.constantize(type) 213 | end 214 | 215 | # @api private 216 | def self.inflector 217 | @inflector ||= 218 | begin 219 | require 'dry/inflector' 220 | Dry::Inflector.new 221 | rescue LoadError 222 | raise( 223 | NotImplementedError, 224 | 'Virtus needs dry-inflector gem to constantize namespaced constant names' 225 | ) 226 | end 227 | end 228 | 229 | # Finalize pending attributes 230 | # 231 | # @example 232 | # class User 233 | # include Virtus.model(:finalize => false) 234 | # 235 | # attribute :address, 'Address' 236 | # end 237 | # 238 | # class Address 239 | # include Virtus.model(:finalize => false) 240 | # 241 | # attribute :user, 'User' 242 | # end 243 | # 244 | # Virtus.finalize # this will resolve constant names 245 | # 246 | # @return [Array] array of finalized models 247 | # 248 | # @api public 249 | def self.finalize 250 | Builder.pending.each do |klass| 251 | klass.attribute_set.finalize 252 | end 253 | end 254 | 255 | # @api private 256 | def self.warn(msg) 257 | Kernel.warn(msg) 258 | end 259 | 260 | end # module Virtus 261 | 262 | require 'descendants_tracker' 263 | require 'axiom-types' 264 | require 'coercible' 265 | 266 | require 'virtus/support/equalizer' 267 | require 'virtus/support/options' 268 | require 'virtus/support/type_lookup' 269 | 270 | require 'virtus/model' 271 | require 'virtus/extensions' 272 | require 'virtus/const_missing_extensions' 273 | require 'virtus/class_inclusions' 274 | require 'virtus/module_extensions' 275 | 276 | require 'virtus/configuration' 277 | require 'virtus/builder' 278 | require 'virtus/builder/hook_context' 279 | 280 | require 'virtus/class_methods' 281 | require 'virtus/instance_methods' 282 | 283 | require 'virtus/value_object' 284 | 285 | require 'virtus/coercer' 286 | require 'virtus/attribute_set' 287 | 288 | require 'virtus/attribute/default_value' 289 | require 'virtus/attribute/default_value/from_clonable' 290 | require 'virtus/attribute/default_value/from_callable' 291 | require 'virtus/attribute/default_value/from_symbol' 292 | 293 | require 'virtus/attribute' 294 | require 'virtus/attribute/builder' 295 | require 'virtus/attribute/coercer' 296 | require 'virtus/attribute/accessor' 297 | require 'virtus/attribute/coercible' 298 | require 'virtus/attribute/strict' 299 | require 'virtus/attribute/lazy_default' 300 | require 'virtus/attribute/nullify_blank' 301 | 302 | require 'virtus/attribute/boolean' 303 | require 'virtus/attribute/collection' 304 | require 'virtus/attribute/hash' 305 | require 'virtus/attribute/embedded_value' 306 | -------------------------------------------------------------------------------- /lib/virtus/attribute.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Attribute objects handle coercion and provide interface to hook into an 4 | # attribute set instance that's included into a class or object 5 | # 6 | # @example 7 | # 8 | # # non-strict mode 9 | # attr = Virtus::Attribute.build(Integer) 10 | # attr.coerce('1') 11 | # # => 1 12 | # 13 | # # strict mode 14 | # attr = Virtus::Attribute.build(Integer, :strict => true) 15 | # attr.coerce('not really coercible') 16 | # # => Virtus::CoercionError: Failed to coerce "not really coercible" into Integer 17 | # 18 | class Attribute 19 | extend DescendantsTracker, Options, TypeLookup 20 | 21 | include Equalizer.new(inspect) << :type << :options 22 | 23 | accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize, :nullify_blank 24 | 25 | strict false 26 | required true 27 | accessor :public 28 | finalize true 29 | nullify_blank false 30 | 31 | # @see Virtus.coerce 32 | # 33 | # @deprecated 34 | # 35 | # @api public 36 | def self.coerce(value = Undefined) 37 | Virtus.warn "#{self}.coerce is deprecated and will be removed in 1.0.0. Use Virtus.coerce instead: ##{caller.first}" 38 | return Virtus.coerce if value.equal?(Undefined) 39 | Virtus.coerce = value 40 | self 41 | end 42 | 43 | # Return type of this attribute 44 | # 45 | # @return [Axiom::Types::Type] 46 | # 47 | # @api public 48 | attr_reader :type 49 | 50 | # @api private 51 | attr_reader :primitive, :options, :default_value, :coercer 52 | 53 | # Builds an attribute instance 54 | # 55 | # @param [Class,Array,Hash,String,Symbol] type 56 | # this can be an explicit class or an object from which virtus can infer 57 | # the type 58 | # 59 | # @param [#to_hash] options 60 | # optional extra options hash 61 | # 62 | # @return [Attribute] 63 | # 64 | # @api public 65 | def self.build(type, options = {}) 66 | Builder.call(type, options) 67 | end 68 | 69 | # @api private 70 | def self.build_coercer(type, options = {}) 71 | Coercer.new(type, options.fetch(:configured_coercer) { Virtus.coercer }) 72 | end 73 | 74 | # @api private 75 | def self.build_type(definition) 76 | Axiom::Types.infer(definition.primitive) 77 | end 78 | 79 | # @api private 80 | def self.merge_options!(*) 81 | # noop 82 | end 83 | 84 | # @api private 85 | def initialize(type, options) 86 | @type = type 87 | @primitive = type.primitive 88 | @options = options 89 | @default_value = options.fetch(:default_value) 90 | @coercer = options.fetch(:coercer) 91 | end 92 | 93 | # Coerce the input into the expected type 94 | # 95 | # @example 96 | # 97 | # attr = Virtus::Attribute.build(String) 98 | # attr.coerce(:one) # => 'one' 99 | # 100 | # @param [Object] input 101 | # 102 | # @api public 103 | def coerce(input) 104 | coercer.call(input) 105 | end 106 | 107 | # Return a new attribute with the new name 108 | # 109 | # @param [Symbol] name 110 | # 111 | # @return [Attribute] 112 | # 113 | # @api public 114 | def rename(name) 115 | self.class.build(type, options.merge(:name => name)) 116 | end 117 | 118 | # Return if the given value was coerced 119 | # 120 | # @param [Object] value 121 | # 122 | # @return [Boolean] 123 | # 124 | # @api public 125 | def value_coerced?(value) 126 | coercer.success?(primitive, value) 127 | end 128 | 129 | # Return if the attribute is coercible 130 | # 131 | # @example 132 | # 133 | # attr = Virtus::Attribute.build(String, :coerce => true) 134 | # attr.coercible? # => true 135 | # 136 | # attr = Virtus::Attribute.build(String, :coerce => false) 137 | # attr.coercible? # => false 138 | # 139 | # @return [Boolean] 140 | # 141 | # @api public 142 | def coercible? 143 | kind_of?(Coercible) 144 | end 145 | 146 | # Return if the attribute has lazy default value evaluation 147 | # 148 | # @example 149 | # 150 | # attr = Virtus::Attribute.build(String, :lazy => true) 151 | # attr.lazy? # => true 152 | # 153 | # attr = Virtus::Attribute.build(String, :lazy => false) 154 | # attr.lazy? # => false 155 | # 156 | # @return [Boolean] 157 | # 158 | # @api public 159 | def lazy? 160 | kind_of?(LazyDefault) 161 | end 162 | 163 | # Return if the attribute is in the strict coercion mode 164 | # 165 | # @example 166 | # 167 | # attr = Virtus::Attribute.build(String, :strict => true) 168 | # attr.strict? # => true 169 | # 170 | # attr = Virtus::Attribute.build(String, :strict => false) 171 | # attr.strict? # => false 172 | # 173 | # @return [Boolean] 174 | # 175 | # @api public 176 | def strict? 177 | kind_of?(Strict) 178 | end 179 | 180 | # Return if the attribute is in the nullify blank coercion mode 181 | # 182 | # @example 183 | # 184 | # attr = Virtus::Attribute.build(String, :nullify_blank => true) 185 | # attr.nullify_blank? # => true 186 | # 187 | # attr = Virtus::Attribute.build(String, :nullify_blank => false) 188 | # attr.nullify_blank? # => false 189 | # 190 | # @return [Boolean] 191 | # 192 | # @api public 193 | def nullify_blank? 194 | kind_of?(NullifyBlank) 195 | end 196 | 197 | # Return if the attribute is accepts nil values as valid coercion output 198 | # 199 | # @example 200 | # 201 | # attr = Virtus::Attribute.build(String, :required => true) 202 | # attr.required? # => true 203 | # 204 | # attr = Virtus::Attribute.build(String, :required => false) 205 | # attr.required? # => false 206 | # 207 | # @return [Boolean] 208 | # 209 | # @api public 210 | def required? 211 | options[:required] 212 | end 213 | 214 | # Return if the attribute was already finalized 215 | # 216 | # @example 217 | # 218 | # attr = Virtus::Attribute.build(String, :finalize => true) 219 | # attr.finalized? # => true 220 | # 221 | # attr = Virtus::Attribute.build(String, :finalize => false) 222 | # attr.finalized? # => false 223 | # 224 | # @return [Boolean] 225 | # 226 | # @api public 227 | def finalized? 228 | frozen? 229 | end 230 | 231 | # @api private 232 | def define_accessor_methods(attribute_set) 233 | attribute_set.define_reader_method(self, name, options[:reader]) 234 | attribute_set.define_writer_method(self, "#{name}=", options[:writer]) 235 | end 236 | 237 | # @api private 238 | def finalize 239 | freeze 240 | self 241 | end 242 | 243 | end # class Attribute 244 | 245 | end # module Virtus 246 | -------------------------------------------------------------------------------- /lib/virtus/attribute/accessor.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Accessor extension provides methods to read and write attributes 5 | # 6 | # @example 7 | # 8 | # attribute = Virtus::Attribute.build(String, :name => :email) 9 | # model = Class.new { attr_reader :email } 10 | # object = model.new 11 | # 12 | # attribute.set(object, 'jane@doe.com') 13 | # attribute.get(object) # => 'jane@doe.com' 14 | # 15 | module Accessor 16 | 17 | # Return name of this accessor attribute 18 | # 19 | # @return [Symbol] 20 | # 21 | # @api public 22 | attr_reader :name 23 | 24 | # Return instance_variable_name used by this accessor 25 | # 26 | # @api private 27 | attr_reader :instance_variable_name 28 | 29 | # @api private 30 | def self.extended(descendant) 31 | super 32 | name = descendant.options.fetch(:name).to_sym 33 | descendant.instance_variable_set('@name', name) 34 | descendant.instance_variable_set('@instance_variable_name', "@#{name}") 35 | end 36 | 37 | # Return if attribute value is defined 38 | # 39 | # @param [Object] instance 40 | # 41 | # @return [Boolean] 42 | # 43 | # @api public 44 | def defined?(instance) 45 | instance.instance_variable_defined?(instance_variable_name) 46 | end 47 | 48 | # Return value of the attribute 49 | # 50 | # @param [Object] instance 51 | # 52 | # @return [Object] 53 | # 54 | # @api public 55 | def get(instance) 56 | instance.instance_variable_get(instance_variable_name) 57 | end 58 | 59 | # Set value of the attribute 60 | # 61 | # @param [Object] instance 62 | # @param [Object] value 63 | # 64 | # @return [Object] value that was set 65 | # 66 | # @api public 67 | def set(instance, value) 68 | instance.instance_variable_set(instance_variable_name, value) 69 | end 70 | 71 | # Set default value 72 | # 73 | # @param [Object] instance 74 | # 75 | # @return [Object] value that was set 76 | # 77 | # @api public 78 | def set_default_value(instance) 79 | set(instance, default_value.call(instance, self)) 80 | end 81 | 82 | # Returns a Boolean indicating whether the reader method is public 83 | # 84 | # @return [Boolean] 85 | # 86 | # @api private 87 | def public_reader? 88 | options[:reader] == :public 89 | end 90 | 91 | # Returns a Boolean indicating whether the writer method is public 92 | # 93 | # @return [Boolean] 94 | # 95 | # @api private 96 | def public_writer? 97 | options[:writer] == :public 98 | end 99 | 100 | end # Accessor 101 | 102 | end # Attribute 103 | end # Virtus 104 | -------------------------------------------------------------------------------- /lib/virtus/attribute/boolean.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Boolean attribute allows true or false values to be set 5 | # Additionally it adds boolean reader method, like "admin?" 6 | # 7 | # @example 8 | # class Post 9 | # include Virtus 10 | # 11 | # attribute :published, Boolean 12 | # end 13 | # 14 | # post = Post.new(:published => false) 15 | # post.published? # => false 16 | # 17 | class Boolean < Attribute 18 | primitive TrueClass 19 | 20 | # @api private 21 | def self.build_type(*) 22 | Axiom::Types::Boolean 23 | end 24 | 25 | # Returns if the given value is either true or false 26 | # 27 | # @example 28 | # boolean = Virtus::Attribute::Boolean.new(:bool) 29 | # boolean.value_coerced?(true) # => true 30 | # boolean.value_coerced?(false) # => true 31 | # boolean.value_coerced?(1) # => false 32 | # boolean.value_coerced?('true') # => false 33 | # 34 | # @return [Boolean] 35 | # 36 | # @api public 37 | def value_coerced?(value) 38 | value.equal?(true) || value.equal?(false) 39 | end 40 | 41 | # Creates an attribute reader method as a query 42 | # 43 | # @param [Module] mod 44 | # 45 | # @return [undefined] 46 | # 47 | # @api private 48 | def define_accessor_methods(attribute_set) 49 | super 50 | attribute_set.define_reader_method(self, "#{name}?", options[:reader]) 51 | end 52 | 53 | end # class Boolean 54 | end # class Attribute 55 | end # module Virtus 56 | -------------------------------------------------------------------------------- /lib/virtus/attribute/builder.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Attribute placeholder used when type constant is passed as a string or symbol 4 | # 5 | # @private 6 | class PendingAttribute 7 | attr_reader :type, :options, :name 8 | 9 | # @api private 10 | def initialize(type, options) 11 | @type, @options = type.to_s, options 12 | @name = options[:name] 13 | end 14 | 15 | # @api private 16 | def finalize 17 | Attribute::Builder.call(determine_type, options).finalize 18 | end 19 | 20 | # @api private 21 | def finalized? 22 | false 23 | end 24 | 25 | # @api private 26 | def determine_type 27 | if type.include?('::') 28 | Virtus.constantize(type) 29 | else 30 | Object.const_get(type) 31 | end 32 | end 33 | 34 | end # PendingAttribute 35 | 36 | # Extracts the actual type primitive from input type 37 | # 38 | # @private 39 | class TypeDefinition 40 | attr_reader :type, :primitive 41 | 42 | # @api private 43 | def initialize(type) 44 | @type = type 45 | initialize_primitive 46 | end 47 | 48 | # @api private 49 | def pending? 50 | @pending if defined?(@pending) 51 | end 52 | 53 | private 54 | 55 | # @api private 56 | def initialize_primitive 57 | @primitive = 58 | if type.instance_of?(String) || type.instance_of?(Symbol) 59 | if !type.to_s.include?('::') && Object.const_defined?(type) 60 | Object.const_get(type) 61 | elsif not Attribute::Builder.determine_type(type) 62 | @pending = true 63 | type 64 | else 65 | type 66 | end 67 | elsif not type.is_a?(Class) 68 | type.class 69 | else 70 | type 71 | end 72 | end 73 | end 74 | 75 | class Attribute 76 | 77 | # Builder is used to set up an attribute instance based on input type and options 78 | # 79 | # @private 80 | class Builder 81 | attr_reader :attribute, :options, :type_definition, :klass, :type 82 | 83 | # @api private 84 | def self.call(type, options = {}) 85 | type_definition = TypeDefinition.new(type) 86 | 87 | if type_definition.pending? 88 | PendingAttribute.new(type, options) 89 | else 90 | new(type_definition, options).attribute 91 | end 92 | end 93 | 94 | # @api private 95 | def self.determine_type(klass, default = nil) 96 | type = Attribute.determine_type(klass) 97 | 98 | if klass.is_a?(Class) 99 | type ||= 100 | if klass < Axiom::Types::Type 101 | determine_type(klass.primitive) 102 | elsif EmbeddedValue.handles?(klass) 103 | EmbeddedValue 104 | elsif klass < Enumerable && !(klass <= Range) 105 | Collection 106 | end 107 | end 108 | 109 | type || default 110 | end 111 | 112 | # @api private 113 | def initialize(type_definition, options) 114 | @type_definition = type_definition 115 | 116 | initialize_class 117 | initialize_type 118 | initialize_options(options) 119 | initialize_default_value 120 | initialize_coercer 121 | initialize_attribute 122 | end 123 | 124 | private 125 | 126 | # @api private 127 | def initialize_class 128 | @klass = self.class.determine_type(type_definition.primitive, Attribute) 129 | end 130 | 131 | # @api private 132 | def initialize_type 133 | @type = klass.build_type(type_definition) 134 | end 135 | 136 | # @api private 137 | def initialize_options(options) 138 | @options = klass.options.merge(:coerce => Virtus.coerce).update(options) 139 | klass.merge_options!(type, @options) 140 | determine_visibility 141 | end 142 | 143 | # @api private 144 | def initialize_default_value 145 | options.update(:default_value => DefaultValue.build(options[:default])) 146 | end 147 | 148 | # @api private 149 | def initialize_coercer 150 | options.update(:coercer => determine_coercer) 151 | end 152 | 153 | # @api private 154 | def initialize_attribute 155 | @attribute = klass.new(type, options) 156 | 157 | @attribute.extend(Accessor) if options[:name] 158 | @attribute.extend(Coercible) if options[:coerce] 159 | @attribute.extend(NullifyBlank) if options[:nullify_blank] 160 | @attribute.extend(Strict) if options[:strict] 161 | @attribute.extend(LazyDefault) if options[:lazy] 162 | 163 | @attribute.finalize if options[:finalize] 164 | end 165 | 166 | # @api private 167 | def determine_coercer 168 | options.fetch(:coercer) { klass.build_coercer(type, options) } 169 | end 170 | 171 | # @api private 172 | def determine_visibility 173 | default_accessor = options.fetch(:accessor) 174 | reader_visibility = options.fetch(:reader, default_accessor) 175 | writer_visibility = options.fetch(:writer, default_accessor) 176 | options.update(:reader => reader_visibility, :writer => writer_visibility) 177 | end 178 | 179 | end # class Builder 180 | 181 | end # class Attribute 182 | end # module Virtus 183 | -------------------------------------------------------------------------------- /lib/virtus/attribute/coercer.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Coercer accessor wrapper 5 | # 6 | # @api private 7 | class Coercer < Virtus::Coercer 8 | 9 | # @api private 10 | attr_reader :method, :coercers 11 | 12 | # Initialize a new coercer object 13 | # 14 | # @param [Object] coercers accessor 15 | # @param [Symbol] coercion method 16 | # 17 | # @return [undefined] 18 | # 19 | # @api private 20 | def initialize(type, coercers) 21 | super(type) 22 | @method = type.coercion_method 23 | @coercers = coercers 24 | end 25 | 26 | # Coerce given value 27 | # 28 | # @return [Object] 29 | # 30 | # @api private 31 | def call(value) 32 | coercers[value.class].public_send(method, value) 33 | rescue ::Coercible::UnsupportedCoercion 34 | value 35 | end 36 | 37 | # @api public 38 | def success?(primitive, value) 39 | coercers[primitive].coerced?(value) 40 | end 41 | 42 | end # class Coercer 43 | 44 | end # class Attribute 45 | end # module Virtus 46 | -------------------------------------------------------------------------------- /lib/virtus/attribute/coercible.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Attribute extension providing coercion when setting an attribute value 5 | # 6 | module Coercible 7 | 8 | # Coerce value before setting 9 | # 10 | # @see Accessor#set 11 | # 12 | # @api public 13 | def set(instance, value) 14 | super(instance, coerce(value)) 15 | end 16 | 17 | end # Coercible 18 | 19 | end # Attribute 20 | end # Virtus 21 | -------------------------------------------------------------------------------- /lib/virtus/attribute/collection.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Collection attribute handles enumerable-like types 5 | # 6 | # Handles coercing members to the designated member type. 7 | # 8 | class Collection < Attribute 9 | default Proc.new { |_, attribute| attribute.primitive.new } 10 | 11 | # @api private 12 | attr_reader :member_type 13 | 14 | # FIXME: temporary hack, remove when Axiom::Type works with EV as member_type 15 | Type = Struct.new(:primitive, :member_type) do 16 | def self.infer(type, primitive) 17 | return type if axiom_type?(type) 18 | 19 | klass = Axiom::Types.infer(type) 20 | member = infer_member_type(type) || Object 21 | 22 | if EmbeddedValue.handles?(member) || pending?(member) 23 | Type.new(primitive, member) 24 | else 25 | klass.new { 26 | primitive primitive 27 | member_type Axiom::Types.infer(member) 28 | } 29 | end 30 | end 31 | 32 | def self.pending?(primitive) 33 | primitive.is_a?(String) || primitive.is_a?(Symbol) 34 | end 35 | 36 | def self.axiom_type?(type) 37 | type.is_a?(Class) && type < Axiom::Types::Type 38 | end 39 | 40 | def self.infer_member_type(type) 41 | return unless type.respond_to?(:count) 42 | 43 | member_type = 44 | if type.count > 1 45 | raise NotImplementedError, "build SumType from list of types (#{type})" 46 | else 47 | type.first 48 | end 49 | 50 | if member_type.is_a?(Class) && member_type < Attribute && member_type.primitive 51 | member_type.primitive 52 | else 53 | member_type 54 | end 55 | end 56 | 57 | def coercion_method 58 | :to_array 59 | end 60 | end 61 | 62 | # @api private 63 | def self.build_type(definition) 64 | Type.infer(definition.type, definition.primitive) 65 | end 66 | 67 | # @api private 68 | def self.merge_options!(type, options) 69 | options[:member_type] ||= Attribute.build(type.member_type, strict: options[:strict]) 70 | end 71 | 72 | # @api public 73 | def coerce(value) 74 | coerced = super 75 | 76 | return coerced unless coerced.respond_to?(:each_with_object) 77 | 78 | coerced.each_with_object(primitive.new) do |entry, collection| 79 | collection << member_type.coerce(entry) 80 | end 81 | end 82 | 83 | # @api public 84 | def value_coerced?(value) 85 | super && value.all? { |item| member_type.value_coerced? item } 86 | end 87 | 88 | # @api private 89 | def finalize 90 | return self if finalized? 91 | @member_type = @options[:member_type].finalize 92 | super 93 | end 94 | 95 | # @api private 96 | def finalized? 97 | super && member_type.finalized? 98 | end 99 | 100 | end # class Collection 101 | 102 | end # class Attribute 103 | end # module Virtus 104 | -------------------------------------------------------------------------------- /lib/virtus/attribute/default_value.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Class representing the default value option 5 | # 6 | # @api private 7 | class DefaultValue 8 | extend DescendantsTracker 9 | 10 | include Equalizer.new(inspect) << :value 11 | 12 | # Builds a default value instance 13 | # 14 | # @return [Virtus::Attribute::DefaultValue] 15 | # 16 | # @api private 17 | def self.build(*args) 18 | klass = descendants.detect { |descendant| descendant.handle?(*args) } || self 19 | klass.new(*args) 20 | end 21 | 22 | # Returns the value instance 23 | # 24 | # @return [Object] 25 | # 26 | # @api private 27 | attr_reader :value 28 | 29 | # Initializes an default value instance 30 | # 31 | # @param [Object] value 32 | # 33 | # @return [undefined] 34 | # 35 | # @api private 36 | def initialize(value) 37 | @value = value 38 | end 39 | 40 | # Evaluates the value 41 | # 42 | # @return [Object] evaluated value 43 | # 44 | # @api private 45 | def call(*) 46 | value 47 | end 48 | 49 | end # class DefaultValue 50 | end # class Attribute 51 | end # module Virtus 52 | -------------------------------------------------------------------------------- /lib/virtus/attribute/default_value/from_callable.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | class DefaultValue 4 | 5 | # Represents default value evaluated via a callable object 6 | # 7 | # @api private 8 | class FromCallable < DefaultValue 9 | 10 | # Return if the class can handle the value 11 | # 12 | # @param [Object] value 13 | # 14 | # @return [Boolean] 15 | # 16 | # @api private 17 | def self.handle?(value) 18 | value.respond_to?(:call) 19 | end 20 | 21 | # Evaluates the value via value#call 22 | # 23 | # @param [Object] args 24 | # 25 | # @return [Object] evaluated value 26 | # 27 | # @api private 28 | def call(*args) 29 | @value.call(*args) 30 | end 31 | 32 | end # class FromCallable 33 | end # class DefaultValue 34 | end # class Attribute 35 | end # module Virtus 36 | -------------------------------------------------------------------------------- /lib/virtus/attribute/default_value/from_clonable.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | class DefaultValue 4 | 5 | # Represents default value evaluated via a clonable object 6 | # 7 | # @api private 8 | class FromClonable < DefaultValue 9 | SINGLETON_CLASSES = [ 10 | ::NilClass, ::TrueClass, ::FalseClass, ::Numeric, ::Symbol ].freeze 11 | 12 | # Return if the class can handle the value 13 | # 14 | # @param [Object] value 15 | # 16 | # @return [Boolean] 17 | # 18 | # @api private 19 | def self.handle?(value) 20 | SINGLETON_CLASSES.none? { |klass| value.kind_of?(klass) } 21 | end 22 | 23 | # Evaluates the value via value#clone 24 | # 25 | # @return [Object] evaluated value 26 | # 27 | # @api private 28 | def call(*) 29 | @value.clone 30 | end 31 | 32 | end # class FromClonable 33 | end # class DefaultValue 34 | end # class Attribute 35 | end # module Virtus 36 | -------------------------------------------------------------------------------- /lib/virtus/attribute/default_value/from_symbol.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | class DefaultValue 4 | 5 | # Represents default value evaluated via a symbol 6 | # 7 | # @api private 8 | class FromSymbol < DefaultValue 9 | 10 | # Return if the class can handle the value 11 | # 12 | # @param [Object] value 13 | # 14 | # @return [Boolean] 15 | # 16 | # @api private 17 | def self.handle?(value) 18 | value.is_a?(Symbol) 19 | end 20 | 21 | # Evaluates the value via instance#public_send(value) 22 | # 23 | # Symbol value is returned if the instance doesn't respond to value 24 | # 25 | # @return [Object] evaluated value 26 | # 27 | # @api private 28 | def call(instance, _) 29 | instance.respond_to?(@value, true) ? instance.send(@value) : @value 30 | end 31 | 32 | end # class FromSymbol 33 | end # class DefaultValue 34 | end # class Attribute 35 | end # module Virtus 36 | -------------------------------------------------------------------------------- /lib/virtus/attribute/embedded_value.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # EmbeddedValue handles virtus-like objects, OpenStruct and Struct 5 | # 6 | class EmbeddedValue < Attribute 7 | TYPES = [Struct, OpenStruct, Virtus, Model::Constructor].freeze 8 | 9 | # Builds Struct-like instance with attributes passed to the constructor as 10 | # a list of args rather than a hash 11 | # 12 | # @private 13 | class FromStruct < Virtus::Coercer 14 | 15 | # @api public 16 | def call(input) 17 | if input.kind_of?(primitive) 18 | input 19 | elsif not input.nil? 20 | primitive.new(*input) 21 | end 22 | end 23 | 24 | end # FromStruct 25 | 26 | # Builds OpenStruct-like instance with attributes passed to the constructor 27 | # as a hash 28 | # 29 | # @private 30 | class FromOpenStruct < Virtus::Coercer 31 | 32 | # @api public 33 | def call(input) 34 | if input.kind_of?(primitive) 35 | input 36 | elsif not input.nil? 37 | primitive.new(input) 38 | end 39 | end 40 | 41 | end # FromOpenStruct 42 | 43 | # @api private 44 | def self.handles?(klass) 45 | klass.is_a?(Class) && TYPES.any? { |type| klass <= type } 46 | end 47 | 48 | # @api private 49 | def self.build_type(definition) 50 | Axiom::Types::Object.new { primitive definition.primitive } 51 | end 52 | 53 | # @api private 54 | def self.build_coercer(type, _options) 55 | primitive = type.primitive 56 | 57 | if primitive < Virtus || primitive < Model::Constructor || primitive <= OpenStruct 58 | FromOpenStruct.new(type) 59 | elsif primitive < Struct 60 | FromStruct.new(type) 61 | end 62 | end 63 | 64 | end # class EmbeddedValue 65 | 66 | end # class Attribute 67 | end # module Virtus 68 | -------------------------------------------------------------------------------- /lib/virtus/attribute/hash.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Handles attributes with Hash type 5 | # 6 | class Hash < Attribute 7 | primitive ::Hash 8 | default primitive.new 9 | 10 | # @api private 11 | attr_reader :key_type, :value_type 12 | 13 | # FIXME: remove this once axiom-types supports it 14 | # 15 | # @private 16 | Type = Struct.new(:key_type, :value_type) do 17 | def self.infer(type) 18 | if axiom_type?(type) 19 | new(type.key_type, type.value_type) 20 | else 21 | type_options = infer_key_and_value_types(type) 22 | key_class = determine_type(type_options.fetch(:key_type, Object)) 23 | value_class = determine_type(type_options.fetch(:value_type, Object)) 24 | 25 | new(key_class, value_class) 26 | end 27 | end 28 | 29 | # @api private 30 | def self.pending?(primitive) 31 | primitive.is_a?(String) || primitive.is_a?(Symbol) 32 | end 33 | 34 | # @api private 35 | def self.axiom_type?(type) 36 | type.is_a?(Class) && type < Axiom::Types::Type 37 | end 38 | 39 | # @api private 40 | def self.determine_type(type) 41 | return type if pending?(type) 42 | 43 | if EmbeddedValue.handles?(type) 44 | type 45 | else 46 | Axiom::Types.infer(type) 47 | end 48 | end 49 | 50 | # @api private 51 | def self.infer_key_and_value_types(type) 52 | return {} unless type.kind_of?(::Hash) 53 | 54 | if type.size > 1 55 | raise ArgumentError, "more than one [key => value] pair in `#{type}`" 56 | else 57 | key_type, value_type = type.keys.first, type.values.first 58 | 59 | key_primitive = 60 | if key_type.is_a?(Class) && key_type < Attribute && key_type.primitive 61 | key_type.primitive 62 | else 63 | key_type 64 | end 65 | 66 | value_primitive = 67 | if value_type.is_a?(Class) && value_type < Attribute && value_type.primitive 68 | value_type.primitive 69 | else 70 | value_type 71 | end 72 | 73 | { :key_type => key_primitive, :value_type => value_primitive} 74 | end 75 | end 76 | 77 | # @api private 78 | def coercion_method 79 | :to_hash 80 | end 81 | 82 | # @api private 83 | def primitive 84 | ::Hash 85 | end 86 | end 87 | 88 | # @api private 89 | def self.build_type(definition) 90 | Type.infer(definition.type) 91 | end 92 | 93 | # @api private 94 | def self.merge_options!(type, options) 95 | options[:key_type] ||= Attribute.build(type.key_type, :strict => options[:strict]) 96 | options[:value_type] ||= Attribute.build(type.value_type, :strict => options[:strict]) 97 | end 98 | 99 | # Coerce members 100 | # 101 | # @see [Attribute#coerce] 102 | # 103 | # @api public 104 | def coerce(*) 105 | coerced = super 106 | 107 | return coerced unless coerced.respond_to?(:each_with_object) 108 | 109 | coerced.each_with_object({}) do |(key, value), hash| 110 | hash[key_type.coerce(key)] = value_type.coerce(value) 111 | end 112 | end 113 | 114 | # @api private 115 | def finalize 116 | return self if finalized? 117 | @key_type = options[:key_type].finalize 118 | @value_type = options[:value_type].finalize 119 | super 120 | end 121 | 122 | # @api private 123 | def finalized? 124 | super && key_type.finalized? && value_type.finalized? 125 | end 126 | 127 | end # class Hash 128 | 129 | end # class Attribute 130 | end # module Virtus 131 | -------------------------------------------------------------------------------- /lib/virtus/attribute/lazy_default.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | module LazyDefault 5 | 6 | # @api public 7 | def get(instance) 8 | if instance.instance_variable_defined?(instance_variable_name) 9 | super 10 | else 11 | set_default_value(instance) 12 | end 13 | end 14 | 15 | end # LazyDefault 16 | 17 | end # Attribute 18 | end # Virtus 19 | -------------------------------------------------------------------------------- /lib/virtus/attribute/nullify_blank.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Attribute extension which nullifies blank attributes when coercion failed 5 | # 6 | module NullifyBlank 7 | 8 | # @see [Attribute#coerce] 9 | # 10 | # @api public 11 | def coerce(input) 12 | output = super 13 | 14 | if !value_coerced?(output) && input.to_s.empty? 15 | nil 16 | else 17 | output 18 | end 19 | end 20 | 21 | end # NullifyBlank 22 | 23 | end # Attribute 24 | end # Virtus 25 | -------------------------------------------------------------------------------- /lib/virtus/attribute/strict.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Attribute 3 | 4 | # Attribute extension which raises CoercionError when coercion failed 5 | # 6 | module Strict 7 | 8 | # @see [Attribute#coerce] 9 | # 10 | # @raises [CoercionError] when coercer failed 11 | # 12 | # @api public 13 | def coerce(*) 14 | output = super 15 | 16 | if value_coerced?(output) || !required? && output.nil? 17 | output 18 | else 19 | raise CoercionError.new(output, self) 20 | end 21 | end 22 | 23 | end # Strict 24 | 25 | end # Attribute 26 | end # Virtus 27 | -------------------------------------------------------------------------------- /lib/virtus/attribute_set.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # A set of Attribute objects 4 | class AttributeSet < Module 5 | include Enumerable 6 | 7 | # @api private 8 | def self.create(descendant) 9 | if descendant.respond_to?(:superclass) && descendant.superclass.respond_to?(:attribute_set) 10 | parent = descendant.superclass.public_send(:attribute_set) 11 | end 12 | descendant.instance_variable_set('@attribute_set', AttributeSet.new(parent)) 13 | end 14 | 15 | # Initialize an AttributeSet 16 | # 17 | # @param [AttributeSet] parent 18 | # @param [Array] attributes 19 | # 20 | # @return [undefined] 21 | # 22 | # @api private 23 | def initialize(parent = nil, attributes = []) 24 | @parent = parent 25 | @attributes = attributes.dup 26 | @index = {} 27 | reset 28 | end 29 | 30 | # Iterate over each attribute in the set 31 | # 32 | # @example 33 | # attribute_set = AttributeSet.new(attributes, parent) 34 | # attribute_set.each { |attribute| ... } 35 | # 36 | # @yield [attribute] 37 | # 38 | # @yieldparam [Attribute] attribute 39 | # each attribute in the set 40 | # 41 | # @return [self] 42 | # 43 | # @api public 44 | def each 45 | return to_enum unless block_given? 46 | @index.each { |name, attribute| yield attribute if name.kind_of?(Symbol) } 47 | self 48 | end 49 | 50 | # Adds the attributes to the set 51 | # 52 | # @example 53 | # attribute_set.merge(attributes) 54 | # 55 | # @param [Array] attributes 56 | # 57 | # @return [self] 58 | # 59 | # @api public 60 | def merge(attributes) 61 | attributes.each { |attribute| self << attribute } 62 | self 63 | end 64 | 65 | # Adds an attribute to the set 66 | # 67 | # @example 68 | # attribute_set << attribute 69 | # 70 | # @param [Attribute] attribute 71 | # 72 | # @return [self] 73 | # 74 | # @api public 75 | def <<(attribute) 76 | self[attribute.name] = attribute 77 | attribute.define_accessor_methods(self) if attribute.finalized? 78 | self 79 | end 80 | 81 | # Get an attribute by name 82 | # 83 | # @example 84 | # attribute_set[:name] # => Attribute object 85 | # 86 | # @param [Symbol] name 87 | # 88 | # @return [Attribute] 89 | # 90 | # @api public 91 | def [](name) 92 | @index[name] 93 | end 94 | 95 | # Set an attribute by name 96 | # 97 | # @example 98 | # attribute_set[:name] = attribute 99 | # 100 | # @param [Symbol] name 101 | # @param [Attribute] attribute 102 | # 103 | # @return [Attribute] 104 | # 105 | # @api public 106 | def []=(name, attribute) 107 | @attributes << attribute 108 | update_index(name, attribute) 109 | end 110 | 111 | # Reset the index when the parent is updated 112 | # 113 | # @return [self] 114 | # 115 | # @api private 116 | def reset 117 | merge_attributes(@parent) if @parent 118 | merge_attributes(@attributes) 119 | self 120 | end 121 | 122 | # Defines an attribute reader method 123 | # 124 | # @param [Attribute] attribute 125 | # @param [Symbol] method_name 126 | # @param [Symbol] visibility 127 | # 128 | # @return [undefined] 129 | # 130 | # @api private 131 | def define_reader_method(attribute, method_name, visibility) 132 | define_method(method_name) { attribute.get(self) } 133 | send(visibility, method_name) 134 | end 135 | 136 | # Defines an attribute writer method 137 | # 138 | # @param [Attribute] attribute 139 | # @param [Symbol] method_name 140 | # @param [Symbol] visibility 141 | # 142 | # @return [undefined] 143 | # 144 | # @api private 145 | def define_writer_method(attribute, method_name, visibility) 146 | define_method(method_name) { |value| attribute.set(self, value) } 147 | send(visibility, method_name) 148 | end 149 | 150 | # Get values of all attributes defined for this class, ignoring privacy 151 | # 152 | # @return [Hash] 153 | # 154 | # @api private 155 | def get(object) 156 | each_with_object({}) do |attribute, attributes| 157 | name = attribute.name 158 | attributes[name] = object.__send__(name) if attribute.public_reader? 159 | end 160 | end 161 | 162 | # Mass-assign attribute values 163 | # 164 | # @see Virtus::InstanceMethods#attributes= 165 | # 166 | # @return [Hash] 167 | # 168 | # @api private 169 | def set(object, attributes) 170 | coerce(attributes).each do |name, value| 171 | writer_name = "#{name}=" 172 | if object.allowed_writer_methods.include?(writer_name) 173 | object.__send__(writer_name, value) 174 | end 175 | end 176 | end 177 | 178 | # Set default attributes 179 | # 180 | # @return [self] 181 | # 182 | # @api private 183 | def set_defaults(object, filter = method(:skip_default?)) 184 | each do |attribute| 185 | next if filter.call(object, attribute) 186 | attribute.set_default_value(object) 187 | end 188 | end 189 | 190 | # Coerce attributes received to a hash 191 | # 192 | # @return [Hash] 193 | # 194 | # @api private 195 | def coerce(attributes) 196 | ::Hash.try_convert(attributes) or raise( 197 | NoMethodError, "Expected #{attributes.inspect} to respond to #to_hash" 198 | ) 199 | end 200 | 201 | # @api private 202 | def finalize 203 | each do |attribute| 204 | self << attribute.finalize unless attribute.finalized? 205 | end 206 | end 207 | 208 | private 209 | 210 | # @api private 211 | def skip_default?(object, attribute) 212 | attribute.lazy? || attribute.defined?(object) 213 | end 214 | 215 | # Merge the attributes into the index 216 | # 217 | # @param [Array] attributes 218 | # 219 | # @return [undefined] 220 | # 221 | # @api private 222 | def merge_attributes(attributes) 223 | attributes.each { |attribute| update_index(attribute.name, attribute) } 224 | end 225 | 226 | # Update the symbol and string indexes with the attribute 227 | # 228 | # @param [Symbol] name 229 | # 230 | # @param [Attribute] attribute 231 | # 232 | # @return [undefined] 233 | # 234 | # @api private 235 | def update_index(name, attribute) 236 | @index[name] = @index[name.to_s.freeze] = attribute 237 | end 238 | 239 | end # class AttributeSet 240 | end # module Virtus 241 | -------------------------------------------------------------------------------- /lib/virtus/builder.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Class to build a Virtus module with it's own config 4 | # 5 | # This allows for individual Virtus modules to be included in 6 | # classes and not impacted by the global Virtus config, 7 | # which is implemented using Virtus::config. 8 | # 9 | # @private 10 | class Builder 11 | 12 | # Return module 13 | # 14 | # @return [Module] 15 | # 16 | # @api private 17 | attr_reader :mod 18 | 19 | # Return config 20 | # 21 | # @return [config] 22 | # 23 | # @api private 24 | attr_reader :config 25 | 26 | # @api private 27 | def self.call(options, &block) 28 | new(Configuration.new(options, &block)).mod 29 | end 30 | 31 | # @api private 32 | def self.pending 33 | @pending ||= [] 34 | end 35 | 36 | # Initializes a new Builder 37 | # 38 | # @param [Configuration] config 39 | # @param [Module] mod 40 | # 41 | # @return [undefined] 42 | # 43 | # @api private 44 | def initialize(conf, mod = Module.new) 45 | @config, @mod = conf, mod 46 | add_included_hook 47 | add_extended_hook 48 | end 49 | 50 | # @api private 51 | def extensions 52 | [Model::Core] 53 | end 54 | 55 | # @api private 56 | def options 57 | config.to_h 58 | end 59 | 60 | private 61 | 62 | # Adds the .included hook to the anonymous module which then defines the 63 | # .attribute method to override the default. 64 | # 65 | # @return [Module] 66 | # 67 | # @api private 68 | def add_included_hook 69 | with_hook_context do |context| 70 | mod.define_singleton_method :included do |object| 71 | Builder.pending << object unless context.finalize? 72 | context.modules.each { |mod| object.send(:include, mod) } 73 | object.define_singleton_method(:attribute, context.attribute_method) 74 | end 75 | end 76 | end 77 | 78 | # @api private 79 | def add_extended_hook 80 | with_hook_context do |context| 81 | mod.define_singleton_method :extended do |object| 82 | context.modules.each { |mod| object.extend(mod) } 83 | object.define_singleton_method(:attribute, context.attribute_method) 84 | end 85 | end 86 | end 87 | 88 | # @api private 89 | def with_hook_context 90 | yield(HookContext.new(self, config)) 91 | end 92 | 93 | end # class Builder 94 | 95 | # @private 96 | class ModelBuilder < Builder 97 | end # ModelBuilder 98 | 99 | # @private 100 | class ModuleBuilder < Builder 101 | 102 | private 103 | 104 | # @api private 105 | def add_included_hook 106 | with_hook_context do |context| 107 | mod.define_singleton_method :included do |object| 108 | super(object) 109 | object.extend(ModuleExtensions) 110 | ModuleExtensions.setup(object, context.modules) 111 | object.define_singleton_method(:attribute, context.attribute_method) 112 | end 113 | end 114 | end 115 | 116 | end # ModuleBuilder 117 | 118 | # @private 119 | class ValueObjectBuilder < Builder 120 | 121 | # @api private 122 | def extensions 123 | super << ValueObject::AllowedWriterMethods << ValueObject::InstanceMethods 124 | end 125 | 126 | # @api private 127 | def options 128 | super.merge(:writer => :private) 129 | end 130 | 131 | end # ValueObjectBuilder 132 | 133 | end # module Virtus 134 | -------------------------------------------------------------------------------- /lib/virtus/builder/hook_context.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | class Builder 3 | 4 | # Context used for building "included" and "extended" hooks 5 | # 6 | # @private 7 | class HookContext 8 | attr_reader :builder, :config, :attribute_method 9 | 10 | # @api private 11 | def initialize(builder, config) 12 | @builder, @config = builder, config 13 | initialize_attribute_method 14 | end 15 | 16 | # @api private 17 | def modules 18 | modules = builder.extensions 19 | modules << Model::Constructor if constructor? 20 | modules << Model::MassAssignment if mass_assignment? 21 | modules 22 | end 23 | 24 | # @api private 25 | def constructor? 26 | config.constructor 27 | end 28 | 29 | # @api private 30 | def mass_assignment? 31 | config.mass_assignment 32 | end 33 | 34 | # @api private 35 | def finalize? 36 | config.finalize 37 | end 38 | 39 | # @api private 40 | def initialize_attribute_method 41 | method_options = builder.options 42 | 43 | @attribute_method = lambda do |name, type = nil, options = {}| 44 | super(name, type, method_options.merge(options)) 45 | end 46 | end 47 | 48 | end # HookContext 49 | 50 | end # Builder 51 | end # Virtus 52 | -------------------------------------------------------------------------------- /lib/virtus/class_inclusions.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Class-level extensions 4 | module ClassInclusions 5 | 6 | # Extends a descendant with class and instance methods 7 | # 8 | # @param [Class] descendant 9 | # 10 | # @return [undefined] 11 | # 12 | # @api private 13 | def self.included(descendant) 14 | super 15 | descendant.extend(ClassMethods) 16 | descendant.class_eval { include Methods } 17 | descendant.class_eval { include InstanceMethods } 18 | descendant.class_eval { include InstanceMethods::Constructor } 19 | descendant.class_eval { include InstanceMethods::MassAssignment } 20 | end 21 | private_class_method :included 22 | 23 | module Methods 24 | 25 | # Return a list of allowed writer method names 26 | # 27 | # @return [Set] 28 | # 29 | # @api private 30 | def allowed_writer_methods 31 | self.class.allowed_writer_methods 32 | end 33 | 34 | private 35 | 36 | # Return class' attribute set 37 | # 38 | # @return [Virtus::AttributeSet] 39 | # 40 | # @api private 41 | def attribute_set 42 | self.class.attribute_set 43 | end 44 | 45 | end # Methods 46 | 47 | end # module ClassInclusions 48 | end # module Virtus 49 | -------------------------------------------------------------------------------- /lib/virtus/class_methods.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Class methods that are added when you include Virtus 4 | module ClassMethods 5 | include Extensions::Methods 6 | include ConstMissingExtensions 7 | 8 | # Hook called when module is extended 9 | # 10 | # @param [Class] descendant 11 | # 12 | # @return [undefined] 13 | # 14 | # @api private 15 | def self.extended(descendant) 16 | super 17 | descendant.send(:include, AttributeSet.create(descendant)) 18 | end 19 | private_class_method :extended 20 | 21 | # Returns all the attributes defined on a Class 22 | # 23 | # @example 24 | # class User 25 | # include Virtus 26 | # 27 | # attribute :name, String 28 | # attribute :age, Integer 29 | # end 30 | # 31 | # User.attribute_set # => 32 | # 33 | # TODO: implement inspect so the output is not cluttered - solnic 34 | # 35 | # @return [AttributeSet] 36 | # 37 | # @api public 38 | def attribute_set 39 | @attribute_set 40 | end 41 | 42 | # @see Virtus::ClassMethods.attribute_set 43 | # 44 | # @deprecated 45 | # 46 | # @api public 47 | def attributes 48 | warn "#{self}.attributes is deprecated. Use #{self}.attribute_set instead: #{caller.first}" 49 | attribute_set 50 | end 51 | 52 | private 53 | 54 | # Setup descendants' own Attribute-accessor-method-hosting modules 55 | # 56 | # Descendants inherit Attribute accessor methods via Ruby's inheritance 57 | # mechanism: Attribute accessor methods are defined in a module included 58 | # in a superclass. Attributes defined on descendants add methods to the 59 | # descendant's Attributes accessor module, leaving the superclass's method 60 | # table unaffected. 61 | # 62 | # @param [Class] descendant 63 | # 64 | # @return [undefined] 65 | # 66 | # @api private 67 | def inherited(descendant) 68 | super 69 | AttributeSet.create(descendant) 70 | descendant.module_eval { include attribute_set } 71 | end 72 | 73 | # The list of allowed public methods 74 | # 75 | # @return [Array] 76 | # 77 | # @api private 78 | def allowed_methods 79 | public_instance_methods.map(&:to_s) 80 | end 81 | 82 | # @api private 83 | def assert_valid_name(name) 84 | if instance_methods.include?(:attributes) && name.to_sym == :attributes 85 | raise ArgumentError, "#{name.inspect} is not allowed as an attribute name" 86 | end 87 | end 88 | 89 | end # module ClassMethods 90 | end # module Virtus 91 | -------------------------------------------------------------------------------- /lib/virtus/coercer.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Abstract coercer class 4 | # 5 | class Coercer 6 | include Equalizer.new(inspect) << :primitive << :type 7 | 8 | # @api private 9 | attr_reader :primitive, :type 10 | 11 | # @api private 12 | def initialize(type) 13 | @type = type 14 | @primitive = type.primitive 15 | end 16 | 17 | # Coerce input value into expected primitive type 18 | # 19 | # @param [Object] input 20 | # 21 | # @return [Object] coerced input 22 | # 23 | # @api public 24 | def call(input) 25 | NotImplementedError.new("#{self.class}#call must be implemented") 26 | end 27 | 28 | # Return if the input value was successfuly coerced 29 | # 30 | # @param [Object] input 31 | # 32 | # @return [Object] coerced input 33 | # 34 | # @api public 35 | def success?(primitive, input) 36 | input.kind_of?(primitive) 37 | end 38 | 39 | end # Coercer 40 | 41 | end # Virtus 42 | -------------------------------------------------------------------------------- /lib/virtus/configuration.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # A Configuration instance 4 | class Configuration 5 | 6 | # Access the finalize setting for this instance 7 | attr_accessor :finalize 8 | 9 | # Access the coerce setting for this instance 10 | attr_accessor :coerce 11 | 12 | # Access the strict setting for this instance 13 | attr_accessor :strict 14 | 15 | # Access the nullify_blank setting for this instance 16 | attr_accessor :nullify_blank 17 | 18 | # Access the required setting for this instance 19 | attr_accessor :required 20 | 21 | # Access the constructor setting for this instance 22 | attr_accessor :constructor 23 | 24 | # Access the mass-assignment setting for this instance 25 | attr_accessor :mass_assignment 26 | 27 | # Initialized a configuration instance 28 | # 29 | # @return [undefined] 30 | # 31 | # @api private 32 | def initialize(options={}) 33 | @finalize = options.fetch(:finalize, true) 34 | @coerce = options.fetch(:coerce, true) 35 | @strict = options.fetch(:strict, false) 36 | @nullify_blank = options.fetch(:nullify_blank, false) 37 | @required = options.fetch(:required, true) 38 | @constructor = options.fetch(:constructor, true) 39 | @mass_assignment = options.fetch(:mass_assignment, true) 40 | @coercer = Coercible::Coercer.new 41 | 42 | yield self if block_given? 43 | end 44 | 45 | # Access the coercer for this instance and optional configure a 46 | # new coercer with the passed block 47 | # 48 | # @example 49 | # configuration.coercer do |config| 50 | # config.string.boolean_map = { true => '1', false => '0' } 51 | # end 52 | # 53 | # @return [Coercer] 54 | # 55 | # @api private 56 | def coercer(&block) 57 | @coercer = Coercible::Coercer.new(&block) if block_given? 58 | @coercer 59 | end 60 | 61 | # @api private 62 | def to_h 63 | { :coerce => coerce, 64 | :finalize => finalize, 65 | :strict => strict, 66 | :nullify_blank => nullify_blank, 67 | :required => required, 68 | :configured_coercer => coercer }.freeze 69 | end 70 | 71 | end # class Configuration 72 | end # module Virtus 73 | -------------------------------------------------------------------------------- /lib/virtus/const_missing_extensions.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | module ConstMissingExtensions 3 | 4 | # Hooks into const missing process to determine types of attributes 5 | # 6 | # @param [String] name 7 | # 8 | # @return [Class] 9 | # 10 | # @api private 11 | def const_missing(name) 12 | Attribute::Builder.determine_type(name) or 13 | Axiom::Types.const_defined?(name) && Axiom::Types.const_get(name) or 14 | super 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/virtus/extensions.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Extensions common for both classes and instances 4 | module Extensions 5 | WRITER_METHOD_REGEXP = /=\z/.freeze 6 | INVALID_WRITER_METHODS = %w[ == != === []= attributes= ].to_set.freeze 7 | RESERVED_NAMES = [:attributes].to_set.freeze 8 | 9 | # A hook called when an object is extended with Virtus 10 | # 11 | # @param [Object] object 12 | # 13 | # @return [undefined] 14 | # 15 | # @api private 16 | def self.extended(object) 17 | super 18 | object.instance_eval do 19 | extend Methods 20 | extend InstanceMethods 21 | extend InstanceMethods::MassAssignment 22 | end 23 | end 24 | private_class_method :extended 25 | 26 | module Methods 27 | 28 | # @api private 29 | def self.extended(descendant) 30 | super 31 | descendant.extend(AttributeSet.create(descendant)) 32 | end 33 | private_class_method :extended 34 | 35 | # Defines an attribute on an object's class or instance 36 | # 37 | # @example 38 | # class Book 39 | # include Virtus.model 40 | # 41 | # attribute :title, String 42 | # attribute :author, String 43 | # attribute :published_at, DateTime 44 | # attribute :page_count, Integer 45 | # attribute :index # defaults to Object 46 | # end 47 | # 48 | # @param [Symbol] name 49 | # the name of an attribute 50 | # 51 | # @param [Class,Array,Hash,Axiom::Types::Type,String,Symbol] type 52 | # the type class of an attribute 53 | # 54 | # @param [#to_hash] options 55 | # the extra options hash 56 | # 57 | # @return [self] 58 | # 59 | # @see Attribute.build 60 | # 61 | # @api public 62 | def attribute(name, type = nil, options = {}) 63 | assert_valid_name(name) 64 | attribute_set << Attribute.build(type, options.merge(:name => name)) 65 | self 66 | end 67 | 68 | # @see Virtus.default_value 69 | # 70 | # @api public 71 | def values(&block) 72 | private :attributes= if instance_methods.include?(:attributes=) 73 | yield 74 | include(Equalizer.new(name, attribute_set.map(&:name))) 75 | end 76 | 77 | # The list of writer methods that can be mass-assigned to in #attributes= 78 | # 79 | # @return [Set] 80 | # 81 | # @api private 82 | def allowed_writer_methods 83 | @allowed_writer_methods ||= 84 | begin 85 | allowed_writer_methods = allowed_methods.grep(WRITER_METHOD_REGEXP).to_set 86 | allowed_writer_methods -= INVALID_WRITER_METHODS 87 | allowed_writer_methods.freeze 88 | end 89 | end 90 | 91 | private 92 | 93 | # Return an attribute set for that instance 94 | # 95 | # @return [AttributeSet] 96 | # 97 | # @api private 98 | def attribute_set 99 | @attribute_set 100 | end 101 | 102 | end # Methods 103 | 104 | end # module Extensions 105 | end # module Virtus 106 | -------------------------------------------------------------------------------- /lib/virtus/instance_methods.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Instance methods that are added when you include Virtus 4 | module InstanceMethods 5 | 6 | module Constructor 7 | 8 | # Set attributes during initialization of an object 9 | # 10 | # @param [#to_hash] attributes 11 | # the attributes hash to be set 12 | # 13 | # @return [undefined] 14 | # 15 | # @api private 16 | def initialize(attributes = nil) 17 | attribute_set.set(self, attributes) if attributes 18 | set_default_attributes 19 | end 20 | 21 | end # Constructor 22 | 23 | module MassAssignment 24 | 25 | # Returns a hash of all publicly accessible attributes 26 | # 27 | # @example 28 | # class User 29 | # include Virtus 30 | # 31 | # attribute :name, String 32 | # attribute :age, Integer 33 | # end 34 | # 35 | # user = User.new(:name => 'John', :age => 28) 36 | # user.attributes # => { :name => 'John', :age => 28 } 37 | # 38 | # @return [Hash] 39 | # 40 | # @api public 41 | def attributes 42 | attribute_set.get(self) 43 | end 44 | alias_method :to_hash, :attributes 45 | alias_method :to_h, :attributes 46 | 47 | # Mass-assign attribute values 48 | # 49 | # Keys in the +attributes+ param can be symbols or strings. 50 | # All referenced Attribute writer methods *will* be called. 51 | # Non-attribute setter methods on the receiver *will* be called. 52 | # 53 | # @example 54 | # class User 55 | # include Virtus 56 | # 57 | # attribute :name, String 58 | # attribute :age, Integer 59 | # end 60 | # 61 | # user = User.new 62 | # user.attributes = { :name => 'John', 'age' => 28 } 63 | # 64 | # @param [#to_hash] attributes 65 | # a hash of attribute names and values to set on the receiver 66 | # 67 | # @return [Hash] 68 | # 69 | # @api public 70 | def attributes=(attributes) 71 | attribute_set.set(self, attributes) 72 | end 73 | 74 | end # MassAssignment 75 | 76 | # Returns a value of the attribute with the given name 77 | # 78 | # @example 79 | # class User 80 | # include Virtus 81 | # 82 | # attribute :name, String 83 | # end 84 | # 85 | # user = User.new(:name => 'John') 86 | # user[:name] # => "John" 87 | # 88 | # @param [Symbol] name 89 | # a name of an attribute 90 | # 91 | # @return [Object] 92 | # a value of an attribute 93 | # 94 | # @api public 95 | def [](name) 96 | public_send(name) 97 | end 98 | 99 | # Sets a value of the attribute with the given name 100 | # 101 | # @example 102 | # class User 103 | # include Virtus 104 | # 105 | # attribute :name, String 106 | # end 107 | # 108 | # user = User.new 109 | # user[:name] = "John" # => "John" 110 | # user.name # => "John" 111 | # 112 | # @param [Symbol] name 113 | # a name of an attribute 114 | # 115 | # @param [Object] value 116 | # a value to be set 117 | # 118 | # @return [Object] 119 | # the value set on an object 120 | # 121 | # @api public 122 | def []=(name, value) 123 | public_send("#{name}=", value) 124 | end 125 | 126 | # Freeze object 127 | # 128 | # @return [self] 129 | # 130 | # @api public 131 | # 132 | # @example 133 | # 134 | # class User 135 | # include Virtus 136 | # 137 | # attribute :name, String 138 | # attribute :age, Integer 139 | # end 140 | # 141 | # user = User.new(:name => 'John', :age => 28) 142 | # user.frozen? # => false 143 | # user.freeze 144 | # user.frozen? # => true 145 | # 146 | # @api public 147 | def freeze 148 | set_default_attributes! 149 | super 150 | end 151 | 152 | # Reset an attribute to its default 153 | # 154 | # @return [self] 155 | # 156 | # @api public 157 | # 158 | # @example 159 | # 160 | # class User 161 | # include Virtus 162 | # 163 | # attribute :age, Integer, default: 21 164 | # end 165 | # 166 | # user = User.new(:name => 'John', :age => 28) 167 | # user.age = 30 168 | # user.age # => 30 169 | # user.reset_attribute(:age) 170 | # user.age # => 21 171 | # 172 | # @api public 173 | def reset_attribute(attribute_name) 174 | attribute = attribute_set[attribute_name] 175 | attribute.set_default_value(self) if attribute 176 | self 177 | end 178 | 179 | # Set default attributes 180 | # 181 | # @return [self] 182 | # 183 | # @api private 184 | def set_default_attributes 185 | attribute_set.set_defaults(self) 186 | self 187 | end 188 | 189 | # Set default attributes even lazy ones 190 | # 191 | # @return [self] 192 | # 193 | # @api public 194 | def set_default_attributes! 195 | attribute_set.set_defaults(self, proc { |object, attribute| attribute.defined?(object) }) 196 | self 197 | end 198 | 199 | private 200 | 201 | # The list of allowed public methods 202 | # 203 | # @return [Array] 204 | # 205 | # @api private 206 | def allowed_methods 207 | public_methods.map(&:to_s) 208 | end 209 | 210 | # @api private 211 | def assert_valid_name(name) 212 | if respond_to?(:attributes) && name.to_sym == :attributes || name.to_sym == :attribute_set 213 | raise ArgumentError, "#{name.inspect} is not allowed as an attribute name" 214 | end 215 | end 216 | 217 | end # module InstanceMethods 218 | end # module Virtus 219 | -------------------------------------------------------------------------------- /lib/virtus/model.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | module Model 4 | 5 | # @api private 6 | def self.included(descendant) 7 | super 8 | descendant.send(:include, ClassInclusions) 9 | end 10 | 11 | # @api private 12 | def self.extended(descendant) 13 | super 14 | descendant.extend(Extensions) 15 | end 16 | 17 | module Core 18 | 19 | # @api private 20 | def self.included(descendant) 21 | super 22 | descendant.extend(ClassMethods) 23 | descendant.send(:include, ClassInclusions::Methods) 24 | descendant.send(:include, InstanceMethods) 25 | end 26 | private_class_method :included 27 | 28 | # @api private 29 | def self.extended(descendant) 30 | super 31 | descendant.extend(Extensions::Methods) 32 | descendant.extend(InstanceMethods) 33 | end 34 | private_class_method :extended 35 | 36 | end # Core 37 | 38 | module Constructor 39 | 40 | # @api private 41 | def self.included(descendant) 42 | super 43 | descendant.send(:include, InstanceMethods::Constructor) 44 | end 45 | private_class_method :included 46 | 47 | end # Constructor 48 | 49 | module MassAssignment 50 | 51 | # @api private 52 | def self.included(descendant) 53 | super 54 | descendant.send(:include, InstanceMethods::MassAssignment) 55 | end 56 | private_class_method :included 57 | 58 | # @api private 59 | def self.extended(descendant) 60 | super 61 | descendant.extend(InstanceMethods::MassAssignment) 62 | end 63 | private_class_method :extended 64 | 65 | end # MassAssignment 66 | 67 | end # Model 68 | end # Virtus 69 | -------------------------------------------------------------------------------- /lib/virtus/module_extensions.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Virtus module that can define attributes for later inclusion 4 | # 5 | # @private 6 | module ModuleExtensions 7 | include ConstMissingExtensions 8 | 9 | # @api private 10 | def self.extended(mod) 11 | super 12 | setup(mod) 13 | end 14 | 15 | # @api private 16 | def self.setup(mod, inclusions = [Model], attribute_definitions = []) 17 | mod.instance_variable_set('@inclusions', inclusions) 18 | existing_attributes = mod.instance_variable_get('@attribute_definitions') 19 | new_attributes = (existing_attributes || []) + attribute_definitions 20 | mod.instance_variable_set('@attribute_definitions', new_attributes) 21 | end 22 | 23 | # Define an attribute in the module 24 | # 25 | # @see Virtus::Extensions#attribute 26 | # 27 | # @return [self] 28 | # 29 | # @api private 30 | def attribute(name, type = nil, options = {}) 31 | @attribute_definitions << [name, type, options] 32 | self 33 | end 34 | 35 | private 36 | 37 | # Extend an object with Virtus methods and define attributes 38 | # 39 | # @param [Object] object 40 | # 41 | # @return [undefined] 42 | # 43 | # @api private 44 | def extended(object) 45 | super 46 | @inclusions.each { |mod| object.extend(mod) } 47 | define_attributes(object) 48 | object.set_default_attributes 49 | end 50 | 51 | # Extend a class with Virtus methods and define attributes 52 | # 53 | # @param [Object] object 54 | # 55 | # @return [undefined] 56 | # 57 | # @api private 58 | def included(object) 59 | super 60 | 61 | if Class === object 62 | @inclusions.reject do |mod| 63 | object.ancestors.include?(mod) 64 | end.each do |mod| 65 | object.send(:include, mod) 66 | end 67 | define_attributes(object) 68 | else 69 | object.extend(ModuleExtensions) 70 | ModuleExtensions.setup(object, @inclusions, @attribute_definitions) 71 | end 72 | end 73 | 74 | # Define attributes on a class or instance 75 | # 76 | # @param [Object,Class] object 77 | # 78 | # @return [undefined] 79 | # 80 | # @api private 81 | def define_attributes(object) 82 | @attribute_definitions.each do |attribute_args| 83 | object.attribute(*attribute_args) 84 | end 85 | end 86 | 87 | end # module ModuleExtensions 88 | end # module Virtus 89 | -------------------------------------------------------------------------------- /lib/virtus/support/equalizer.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Define equality, equivalence and inspection methods 4 | class Equalizer < Module 5 | 6 | # Initialize an Equalizer with the given keys 7 | # 8 | # Will use the keys with which it is initialized to define #cmp?, 9 | # #hash, and #inspect 10 | # 11 | # @param [String] name 12 | # 13 | # @param [Array] keys 14 | # 15 | # @return [undefined] 16 | # 17 | # @api private 18 | def initialize(name, keys = []) 19 | @name = name.dup.freeze 20 | @keys = keys.dup 21 | define_methods 22 | include_comparison_methods 23 | end 24 | 25 | # Append a key and compile the equality methods 26 | # 27 | # @return [Equalizer] self 28 | # 29 | # @api private 30 | def <<(key) 31 | @keys << key 32 | self 33 | end 34 | 35 | private 36 | 37 | # Define the equalizer methods based on #keys 38 | # 39 | # @return [undefined] 40 | # 41 | # @api private 42 | def define_methods 43 | define_cmp_method 44 | define_hash_method 45 | define_inspect_method 46 | end 47 | 48 | # Define an #cmp? method based on the instance's values identified by #keys 49 | # 50 | # @return [undefined] 51 | # 52 | # @api private 53 | def define_cmp_method 54 | keys = @keys 55 | define_method(:cmp?) do |comparator, other| 56 | keys.all? { |key| send(key).send(comparator, other.send(key)) } 57 | end 58 | end 59 | 60 | # Define a #hash method based on the instance's values identified by #keys 61 | # 62 | # @return [undefined] 63 | # 64 | # @api private 65 | def define_hash_method 66 | keys = @keys 67 | define_method(:hash) do 68 | keys.map { |key| send(key) }.push(self.class).hash 69 | end 70 | end 71 | 72 | # Define an inspect method that reports the values of the instance's keys 73 | # 74 | # @return [undefined] 75 | # 76 | # @api private 77 | def define_inspect_method 78 | name, keys = @name, @keys 79 | define_method(:inspect) do 80 | "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>" 81 | end 82 | end 83 | 84 | # Include the #eql? and #== methods 85 | # 86 | # @return [undefined] 87 | # 88 | # @api private 89 | def include_comparison_methods 90 | module_eval { include Methods } 91 | end 92 | 93 | # The comparison methods 94 | module Methods 95 | 96 | # Compare the object with other object for equality 97 | # 98 | # @example 99 | # object.eql?(other) # => true or false 100 | # 101 | # @param [Object] other 102 | # the other object to compare with 103 | # 104 | # @return [Boolean] 105 | # 106 | # @api public 107 | def eql?(other) 108 | instance_of?(other.class) && cmp?(__method__, other) 109 | end 110 | 111 | # Compare the object with other object for equivalency 112 | # 113 | # @example 114 | # object == other # => true or false 115 | # 116 | # @param [Object] other 117 | # the other object to compare with 118 | # 119 | # @return [Boolean] 120 | # 121 | # @api public 122 | def ==(other) 123 | other.kind_of?(self.class) && cmp?(__method__, other) 124 | end 125 | 126 | end # module Methods 127 | end # class Equalizer 128 | end # module Virtus 129 | -------------------------------------------------------------------------------- /lib/virtus/support/options.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # A module that adds class and instance level options 4 | module Options 5 | 6 | # Returns default options hash for a given attribute class 7 | # 8 | # @example 9 | # Virtus::Attribute::String.options 10 | # # => {:primitive => String} 11 | # 12 | # @return [Hash] 13 | # a hash of default option values 14 | # 15 | # @api public 16 | def options 17 | accepted_options.each_with_object({}) do |option_name, options| 18 | option_value = send(option_name) 19 | options[option_name] = option_value unless option_value.nil? 20 | end 21 | end 22 | 23 | # Returns an array of valid options 24 | # 25 | # @example 26 | # Virtus::Attribute::String.accepted_options 27 | # # => [:primitive, :accessor, :reader, :writer] 28 | # 29 | # @return [Array] 30 | # the array of valid option names 31 | # 32 | # @api public 33 | def accepted_options 34 | @accepted_options ||= [] 35 | end 36 | 37 | # Defines which options are valid for a given attribute class 38 | # 39 | # @example 40 | # class MyAttribute < Virtus::Attribute 41 | # accept_options :foo, :bar 42 | # end 43 | # 44 | # @return [self] 45 | # 46 | # @api public 47 | def accept_options(*new_options) 48 | add_accepted_options(new_options) 49 | new_options.each { |option| define_option_method(option) } 50 | descendants.each { |descendant| descendant.add_accepted_options(new_options) } 51 | self 52 | end 53 | 54 | protected 55 | 56 | # Adds a reader/writer method for the give option name 57 | # 58 | # @return [undefined] 59 | # 60 | # @api private 61 | def define_option_method(option) 62 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 63 | def self.#{option}(value = Undefined) # def self.primitive(value = Undefined) 64 | @#{option} = nil unless defined?(@#{option}) # @primitive = nil unless defined?(@primitive) 65 | return @#{option} if value.equal?(Undefined) # return @primitive if value.equal?(Undefined) 66 | @#{option} = value # @primitive = value 67 | self # self 68 | end # end 69 | RUBY 70 | end 71 | 72 | # Sets default options 73 | # 74 | # @param [#each] new_options 75 | # options to be set 76 | # 77 | # @return [self] 78 | # 79 | # @api private 80 | def set_options(new_options) 81 | new_options.each { |pair| send(*pair) } 82 | self 83 | end 84 | 85 | # Adds new options that an attribute class can accept 86 | # 87 | # @param [#to_ary] new_options 88 | # new options to be added 89 | # 90 | # @return [self] 91 | # 92 | # @api private 93 | def add_accepted_options(new_options) 94 | accepted_options.concat(new_options) 95 | self 96 | end 97 | 98 | private 99 | 100 | # Adds descendant to descendants array and inherits default options 101 | # 102 | # @param [Class] descendant 103 | # 104 | # @return [undefined] 105 | # 106 | # @api private 107 | def inherited(descendant) 108 | super 109 | descendant.add_accepted_options(accepted_options).set_options(options) 110 | end 111 | 112 | end # module Options 113 | end # module Virtus 114 | -------------------------------------------------------------------------------- /lib/virtus/support/type_lookup.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # A module that adds type lookup to a class 4 | module TypeLookup 5 | 6 | TYPE_FORMAT = /\A[A-Z]\w*\z/.freeze 7 | 8 | # Set cache ivar on the model 9 | # 10 | # @param [Class] model 11 | # 12 | # @return [undefined] 13 | # 14 | # @api private 15 | def self.extended(model) 16 | model.instance_variable_set('@type_lookup_cache', {}) 17 | end 18 | 19 | # Returns a descendant based on a name or class 20 | # 21 | # @example 22 | # MyClass.determine_type('String') # => MyClass::String 23 | # 24 | # @param [Class, #to_s] class_or_name 25 | # name of a class or a class itself 26 | # 27 | # @return [Class] 28 | # a descendant 29 | # 30 | # @return [nil] 31 | # nil if the type cannot be determined by the class_or_name 32 | # 33 | # @api public 34 | def determine_type(class_or_name) 35 | @type_lookup_cache[class_or_name] ||= determine_type_and_cache(class_or_name) 36 | end 37 | 38 | # Return the default primitive supported 39 | # 40 | # @return [Class] 41 | # 42 | # @api private 43 | def primitive 44 | raise NotImplementedError, "#{self}.primitive must be implemented" 45 | end 46 | 47 | private 48 | 49 | # @api private 50 | def determine_type_and_cache(class_or_name) 51 | case class_or_name 52 | when singleton_class 53 | determine_type_from_descendant(class_or_name) 54 | when Class 55 | determine_type_from_primitive(class_or_name) 56 | else 57 | determine_type_from_string(class_or_name.to_s) 58 | end 59 | end 60 | 61 | # Return the class given a descendant 62 | # 63 | # @param [Class] descendant 64 | # 65 | # @return [Class] 66 | # 67 | # @api private 68 | def determine_type_from_descendant(descendant) 69 | descendant if descendant < self 70 | end 71 | 72 | # Return the class given a primitive 73 | # 74 | # @param [Class] primitive 75 | # 76 | # @return [Class] 77 | # 78 | # @return [nil] 79 | # nil if the type cannot be determined by the primitive 80 | # 81 | # @api private 82 | def determine_type_from_primitive(primitive) 83 | type = nil 84 | descendants.select(&:primitive).reverse_each do |descendant| 85 | descendant_primitive = descendant.primitive 86 | next unless primitive <= descendant_primitive 87 | type = descendant if type.nil? or type.primitive > descendant_primitive 88 | end 89 | type 90 | end 91 | 92 | # Return the class given a string 93 | # 94 | # @param [String] string 95 | # 96 | # @return [Class] 97 | # 98 | # @return [nil] 99 | # nil if the type cannot be determined by the string 100 | # 101 | # @api private 102 | def determine_type_from_string(string) 103 | if string =~ TYPE_FORMAT and const_defined?(string, *EXTRA_CONST_ARGS) 104 | const_get(string, *EXTRA_CONST_ARGS) 105 | end 106 | end 107 | 108 | end # module TypeLookup 109 | end # module Virtus 110 | -------------------------------------------------------------------------------- /lib/virtus/value_object.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | 3 | # Include this Module for Value Object semantics 4 | # 5 | # The idea is that instances should be immutable and compared based on state 6 | # (rather than identity, as is typically the case) 7 | # 8 | # @example 9 | # class GeoLocation 10 | # include Virtus::ValueObject 11 | # attribute :latitude, Float 12 | # attribute :longitude, Float 13 | # end 14 | # 15 | # location = GeoLocation.new(:latitude => 10, :longitude => 100) 16 | # same_location = GeoLocation.new(:latitude => 10, :longitude => 100) 17 | # location == same_location #=> true 18 | # hash = { location => :foo } 19 | # hash[same_location] #=> :foo 20 | module ValueObject 21 | 22 | # Callback to configure including Class as a Value Object 23 | # 24 | # Including Class will include Virtus and have additional 25 | # value object semantics defined in this module 26 | # 27 | # @return [Undefined] 28 | # 29 | # TODO: stacking modules is getting painful 30 | # time for Facets' module_inheritance, ActiveSupport::Concern or the like 31 | # 32 | # @api private 33 | def self.included(base) 34 | Virtus.warn "Virtus::ValueObject is deprecated and will be removed in 1.0.0 #{caller.first}" 35 | 36 | base.instance_eval do 37 | include Virtus 38 | include InstanceMethods 39 | extend ClassMethods 40 | extend AllowedWriterMethods 41 | private :attributes= 42 | end 43 | end 44 | 45 | private_class_method :included 46 | 47 | module InstanceMethods 48 | 49 | # ValueObjects are immutable and can't be cloned 50 | # 51 | # They always represent the same value 52 | # 53 | # @example 54 | # 55 | # value_object.clone === value_object # => true 56 | # 57 | # @return [self] 58 | # 59 | # @api public 60 | def clone 61 | self 62 | end 63 | alias dup clone 64 | 65 | # Create a new ValueObject by combining the passed attribute hash with 66 | # the instances attributes. 67 | # 68 | # @example 69 | # 70 | # number = PhoneNumber.new(kind: "mobile", number: "123-456-78-90") 71 | # number.with(number: "987-654-32-10") 72 | # # => # 73 | # 74 | # @return [Object] 75 | # 76 | # @api public 77 | def with(attribute_updates) 78 | self.class.new(attribute_set.get(self).merge(attribute_updates)) 79 | end 80 | 81 | end 82 | 83 | module AllowedWriterMethods 84 | # The list of writer methods that can be mass-assigned to in #attributes= 85 | # 86 | # @return [Set] 87 | # 88 | # @api private 89 | def allowed_writer_methods 90 | @allowed_writer_methods ||= 91 | begin 92 | allowed_writer_methods = super 93 | allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="} 94 | allowed_writer_methods.to_set.freeze 95 | end 96 | end 97 | end 98 | 99 | module ClassMethods 100 | 101 | # Define an attribute on the receiver 102 | # 103 | # The Attribute will have private writer methods (eg., immutable instances) 104 | # and be used in equality/equivalence comparisons 105 | # 106 | # @example 107 | # class GeoLocation 108 | # include Virtus::ValueObject 109 | # 110 | # attribute :latitude, Float 111 | # attribute :longitude, Float 112 | # end 113 | # 114 | # @see Virtus::ClassMethods.attribute 115 | # 116 | # @return [self] 117 | # 118 | # @api public 119 | def attribute(name, type, options = {}) 120 | equalizer << name 121 | super name, type, options.merge(:writer => :private) 122 | end 123 | 124 | # Define and include a module that provides Value Object semantics 125 | # 126 | # Included module will have #inspect, #eql?, #== and #hash 127 | # methods whose definition is based on the _keys_ argument 128 | # 129 | # @example 130 | # virtus_class.equalizer 131 | # 132 | # @return [Equalizer] 133 | # An Equalizer module which defines #inspect, #eql?, #== and #hash 134 | # for instances of this class 135 | # 136 | # @api public 137 | def equalizer 138 | @equalizer ||= 139 | begin 140 | equalizer = Virtus::Equalizer.new(name || inspect) 141 | include equalizer 142 | equalizer 143 | end 144 | end 145 | 146 | end # module ClassMethods 147 | 148 | end # module ValueObject 149 | 150 | end # module Virtus 151 | -------------------------------------------------------------------------------- /lib/virtus/version.rb: -------------------------------------------------------------------------------- 1 | module Virtus 2 | VERSION = '2.0.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/attributes_attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Adding attribute called 'attributes'" do 4 | 5 | context "when mass assignment is disabled" do 6 | before do 7 | module Examples 8 | class User 9 | include Virtus.model(mass_assignment: false) 10 | 11 | attribute :attributes 12 | end 13 | end 14 | end 15 | 16 | it "allows model to use `attributes` attribute" do 17 | user = Examples::User.new 18 | expect(user.attributes).to eq(nil) 19 | user.attributes = "attributes string" 20 | expect(user.attributes).to eq("attributes string") 21 | end 22 | 23 | it "doesn't accept `attributes` key in initializer" do 24 | user = Examples::User.new(attributes: 'attributes string') 25 | expect(user.attributes).to eq(nil) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/integration/building_module_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'I can create a Virtus module' do 4 | before do 5 | module Examples 6 | NoncoercingModule = Virtus.model { |config| 7 | config.coerce = false 8 | } 9 | 10 | CoercingModule = Virtus.model { |config| 11 | config.coerce = true 12 | 13 | config.coercer do |coercer| 14 | coercer.string.boolean_map = { 'yup' => true, 'nope' => false } 15 | end 16 | } 17 | 18 | StrictModule = Virtus.model { |config| 19 | config.strict = true 20 | } 21 | 22 | BlankModule = Virtus.model { |config| 23 | config.nullify_blank = true 24 | } 25 | 26 | class NoncoercedUser 27 | include NoncoercingModule 28 | 29 | attribute :name, String 30 | attribute :happy, String 31 | end 32 | 33 | class CoercedUser 34 | include CoercingModule 35 | 36 | attribute :name, String 37 | attribute :happy, Boolean 38 | end 39 | 40 | class StrictModel 41 | include StrictModule 42 | 43 | attribute :stuff, Hash 44 | attribute :happy, Boolean, :strict => false 45 | end 46 | 47 | class BlankModel 48 | include BlankModule 49 | 50 | attribute :stuff, Hash 51 | attribute :happy, Boolean, :nullify_blank => false 52 | end 53 | end 54 | end 55 | 56 | specify 'including a custom module with coercion disabled' do 57 | user = Examples::NoncoercedUser.new(:name => :Giorgio, :happy => 'yes') 58 | 59 | expect(user.name).to be(:Giorgio) 60 | expect(user.happy).to eql('yes') 61 | end 62 | 63 | specify 'including a custom module with coercion enabled' do 64 | user = Examples::CoercedUser.new(:name => 'Paul', :happy => 'nope') 65 | 66 | expect(user.name).to eql('Paul') 67 | expect(user.happy).to be(false) 68 | end 69 | 70 | specify 'including a custom module with strict enabled' do 71 | model = Examples::StrictModel.new 72 | 73 | expect { model.stuff = 'foo' }.to raise_error(Virtus::CoercionError) 74 | 75 | model.happy = 'foo' 76 | 77 | expect(model.happy).to eql('foo') 78 | end 79 | 80 | specify 'including a custom module with nullify blank enabled' do 81 | model = Examples::BlankModel.new 82 | 83 | model.stuff = '' 84 | expect(model.stuff).to be_nil 85 | 86 | model.happy = 'foo' 87 | 88 | expect(model.happy).to eql('foo') 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/integration/collection_member_coercion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # TODO: refactor to make it inline with the new style of integration specs 4 | 5 | class Address 6 | include Virtus 7 | 8 | attribute :address, String 9 | attribute :locality, String 10 | attribute :region, String 11 | attribute :postal_code, String 12 | end 13 | 14 | class PhoneNumber 15 | include Virtus 16 | 17 | attribute :number, String 18 | end 19 | 20 | class User 21 | include Virtus 22 | 23 | attribute :phone_numbers, Array[PhoneNumber] 24 | attribute :addresses, Set[Address] 25 | end 26 | 27 | describe User do 28 | it { is_expected.to respond_to(:phone_numbers) } 29 | it { is_expected.to respond_to(:phone_numbers=) } 30 | it { is_expected.to respond_to(:addresses) } 31 | it { is_expected.to respond_to(:addresses=) } 32 | 33 | let(:instance) do 34 | described_class.new(:phone_numbers => phone_numbers_attributes, 35 | :addresses => addresses_attributes) 36 | end 37 | 38 | let(:phone_numbers_attributes) { [ 39 | { :number => '212-555-1212' }, 40 | { :number => '919-444-3265' }, 41 | ] } 42 | 43 | let(:addresses_attributes) { [ 44 | { :address => '1234 Any St.', :locality => 'Anytown', :region => "DC", :postal_code => "21234" }, 45 | ] } 46 | 47 | describe '#phone_numbers' do 48 | describe 'first entry' do 49 | subject { instance.phone_numbers.first } 50 | 51 | it { is_expected.to be_instance_of(PhoneNumber) } 52 | 53 | describe '#number' do 54 | subject { super().number } 55 | it { is_expected.to eql('212-555-1212') } 56 | end 57 | end 58 | 59 | describe 'last entry' do 60 | subject { instance.phone_numbers.last } 61 | 62 | it { is_expected.to be_instance_of(PhoneNumber) } 63 | 64 | describe '#number' do 65 | subject { super().number } 66 | it { is_expected.to eql('919-444-3265') } 67 | end 68 | end 69 | end 70 | 71 | describe '#addresses' do 72 | subject { instance.addresses.first } 73 | 74 | it { is_expected.to be_instance_of(Address) } 75 | 76 | describe '#address' do 77 | subject { super().address } 78 | it { is_expected.to eql('1234 Any St.') } 79 | end 80 | 81 | describe '#locality' do 82 | subject { super().locality } 83 | it { is_expected.to eql('Anytown') } 84 | end 85 | 86 | describe '#region' do 87 | subject { super().region } 88 | it { is_expected.to eql('DC') } 89 | end 90 | 91 | describe '#postal_code' do 92 | subject { super().postal_code } 93 | it { is_expected.to eql('21234') } 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/integration/custom_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'custom attributes' do 4 | 5 | before do 6 | module Examples 7 | class NoisyString < Virtus::Attribute 8 | lazy true 9 | 10 | def coerce(input) 11 | input.to_s.upcase 12 | end 13 | end 14 | 15 | class RegularExpression < Virtus::Attribute 16 | primitive Regexp 17 | end 18 | 19 | class User 20 | include Virtus 21 | 22 | attribute :name, String 23 | attribute :scream, NoisyString 24 | attribute :expression, RegularExpression 25 | end 26 | end 27 | end 28 | 29 | subject { Examples::User.new } 30 | 31 | specify 'allows you to define custom attributes' do 32 | regexp = /awesome/ 33 | subject.expression = regexp 34 | expect(subject.expression).to eq(regexp) 35 | end 36 | 37 | specify 'allows you to define coercion methods' do 38 | subject.scream = 'welcome' 39 | expect(subject.scream).to eq('WELCOME') 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /spec/integration/custom_collection_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'custom collection attributes' do 4 | let(:library) { Examples::Library.new } 5 | let(:books) { library.books } 6 | 7 | before do 8 | module Examples end 9 | Examples.const_set 'BookCollection', book_collection_class 10 | 11 | module Examples 12 | class Book 13 | include Virtus 14 | 15 | attribute :title, String 16 | end 17 | 18 | class BookCollectionAttribute < Virtus::Attribute::Collection 19 | primitive BookCollection 20 | end 21 | 22 | class Library 23 | include Virtus 24 | 25 | attribute :books, BookCollection[Book] 26 | end 27 | end 28 | end 29 | 30 | after do 31 | [:BookCollectionAttribute, :BookCollection, :Book, :Library].each do |const| 32 | Examples.send(:remove_const, const) 33 | end 34 | end 35 | 36 | shared_examples_for 'a collection' do 37 | it 'can be used as Virtus attributes' do 38 | attribute = Examples::Library.attribute_set[:books] 39 | expect(attribute).to be_kind_of(Examples::BookCollectionAttribute) 40 | end 41 | 42 | it 'defaults to an empty collection' do 43 | books_should_be_an_empty_collection 44 | end 45 | 46 | it 'coerces nil' do 47 | library.books = nil 48 | books_should_be_an_empty_collection 49 | end 50 | 51 | it 'coerces an empty array' do 52 | library.books = [] 53 | books_should_be_an_empty_collection 54 | end 55 | 56 | it 'coerces an array of attribute hashes' do 57 | library.books = [{ :title => 'Foo' }] 58 | expect(books).to be_kind_of(Examples::BookCollection) 59 | end 60 | 61 | it 'coerces its members' do 62 | library.books = [{ :title => 'Foo' }] 63 | expect(books.count).to eq(1) 64 | expect(books.first).to be_kind_of(Examples::Book) 65 | end 66 | 67 | def books_should_be_an_empty_collection 68 | expect(books).to be_kind_of(Examples::BookCollection) 69 | expect(books.count).to eq(0) 70 | end 71 | end 72 | 73 | context 'with an array subclass' do 74 | let(:book_collection_class) { Class.new(Array) } 75 | 76 | it_behaves_like 'a collection' 77 | end 78 | 79 | context 'with an enumerable' do 80 | require 'forwardable' 81 | 82 | let(:book_collection_class) { 83 | Class.new do 84 | extend Forwardable 85 | include Enumerable 86 | 87 | def_delegators :@array, :each, :<< 88 | 89 | def initialize(*args) 90 | @array = Array[*args] 91 | end 92 | 93 | def self.[](*args) 94 | new(*args) 95 | end 96 | end 97 | } 98 | 99 | it_behaves_like 'a collection' 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/integration/default_values_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "default values" do 4 | 5 | before do 6 | module Examples 7 | 8 | class Reference 9 | include Virtus::ValueObject 10 | 11 | attribute :ref, String 12 | end 13 | 14 | class Page 15 | include Virtus 16 | 17 | attribute :title, String 18 | attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }, :lazy => true 19 | attribute :view_count, Integer, :default => 0 20 | attribute :published, Boolean, :default => false, :accessor => :private 21 | attribute :editor_title, String, :default => :default_editor_title, :lazy => true 22 | attribute :reference, String, :default => Reference.new 23 | attribute :revisions, Array 24 | attribute :index, Hash 25 | attribute :authors, Set 26 | 27 | def default_editor_title 28 | published? ? title : "UNPUBLISHED: #{title}" 29 | end 30 | end 31 | 32 | end 33 | end 34 | 35 | subject { Examples::Page.new } 36 | 37 | specify 'without a default the value is nil' do 38 | expect(subject.title).to be_nil 39 | end 40 | 41 | specify 'can be supplied with the :default option' do 42 | expect(subject.view_count).to eq(0) 43 | end 44 | 45 | specify "you can pass a 'callable-object' to the :default option" do 46 | subject.title = 'Example Blog Post' 47 | expect(subject.slug).to eq('example-blog-post') 48 | end 49 | 50 | specify 'you can set defaults for private attributes' do 51 | subject.title = 'Top Secret' 52 | expect(subject.editor_title).to eq('UNPUBLISHED: Top Secret') 53 | end 54 | 55 | specify 'you can reset attribute to its default' do 56 | subject.view_count = 10 57 | expect do 58 | subject.reset_attribute(:view_count) 59 | end.to change { subject.view_count }.to(0) 60 | end 61 | 62 | context 'a ValueObject' do 63 | it 'does not duplicate the ValueObject' do 64 | page1 = Examples::Page.new 65 | page2 = Examples::Page.new 66 | expect(page1.reference).to equal(page2.reference) 67 | end 68 | end 69 | 70 | context 'an Array' do 71 | specify 'without a default the value is an empty Array' do 72 | expect(subject.revisions).to eql([]) 73 | end 74 | end 75 | 76 | context 'a Hash' do 77 | specify 'without a default the value is an empty Hash' do 78 | expect(subject.index).to eql({}) 79 | end 80 | end 81 | 82 | context 'a Set' do 83 | specify 'without a default the value is an empty Set' do 84 | expect(subject.authors).to eql(Set.new) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/integration/defining_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "virtus attribute definitions" do 4 | 5 | before do 6 | module Examples 7 | class Person 8 | include Virtus 9 | 10 | attribute :name, String 11 | attribute :age, Integer 12 | attribute :doctor, Boolean 13 | attribute :salary, Decimal 14 | end 15 | 16 | class Manager < Person 17 | 18 | end 19 | end 20 | end 21 | 22 | subject(:person) { Examples::Person.new(attributes) } 23 | 24 | let(:attributes) { {} } 25 | 26 | specify 'virtus creates accessor methods' do 27 | person.name = 'Peter' 28 | expect(person.name).to eq('Peter') 29 | end 30 | 31 | specify 'the constructor accepts a hash for mass-assignment' do 32 | john = Examples::Person.new(:name => 'John', :age => 13) 33 | expect(john.name).to eq('John') 34 | expect(john.age).to eq(13) 35 | end 36 | 37 | specify 'Boolean attributes have a predicate method' do 38 | expect(person).not_to be_doctor 39 | person.doctor = true 40 | expect(person).to be_doctor 41 | end 42 | 43 | context 'with attributes' do 44 | let(:attributes) { {:name => 'Jane', :age => 45, :doctor => true, :salary => 4500} } 45 | 46 | specify "#attributes returns the object's attributes as a hash" do 47 | expect(person.attributes).to eq(attributes) 48 | end 49 | 50 | specify "#to_hash returns the object's attributes as a hash" do 51 | expect(person.to_hash).to eq(attributes) 52 | end 53 | 54 | specify "#to_h returns the object's attributes as a hash" do 55 | expect(person.to_h).to eql(attributes) 56 | end 57 | end 58 | 59 | context 'inheritance' do 60 | specify 'inherits all the attributes from the base class' do 61 | fred = Examples::Manager.new(:name => 'Fred', :age => 29) 62 | expect(fred.name).to eq('Fred') 63 | expect(fred.age).to eq(29) 64 | end 65 | 66 | specify 'lets you add attributes to the base class at runtime' do 67 | frank = Examples::Manager.new(:name => 'Frank') 68 | Examples::Person.attribute :just_added, String 69 | frank.just_added = 'it works!' 70 | expect(frank.just_added).to eq('it works!') 71 | end 72 | 73 | specify 'lets you add attributes to the subclass at runtime' do 74 | person_jack = Examples::Person.new(:name => 'Jack') 75 | manager_frank = Examples::Manager.new(:name => 'Frank') 76 | 77 | Examples::Manager.attribute :just_added, String 78 | 79 | manager_frank.just_added = 'awesome!' 80 | 81 | expect(manager_frank.just_added).to eq('awesome!') 82 | expect(person_jack).not_to respond_to(:just_added) 83 | expect(person_jack).not_to respond_to(:just_added=) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/integration/embedded_value_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'embedded values' do 4 | before do 5 | module Examples 6 | class City 7 | include Virtus.model 8 | 9 | attribute :name, String 10 | end 11 | 12 | class Address 13 | include Virtus.model 14 | 15 | attribute :street, String 16 | attribute :zipcode, String 17 | attribute :city, City 18 | end 19 | 20 | class User 21 | include Virtus.model 22 | 23 | attribute :name, String 24 | attribute :address, Address 25 | end 26 | end 27 | end 28 | 29 | subject { Examples::User.new(:name => 'the guy', 30 | :address => address_attributes) } 31 | let(:address_attributes) do 32 | { :street => 'Street 1/2', :zipcode => '12345', :city => { :name => 'NYC' } } 33 | end 34 | 35 | specify '#attributes returns instances of the embedded values' do 36 | expect(subject.attributes).to eq({ 37 | :name => 'the guy', 38 | :address => subject.address 39 | }) 40 | end 41 | 42 | specify 'allows you to pass a hash for the embedded value' do 43 | user = Examples::User.new 44 | user.address = address_attributes 45 | expect(user.address.street).to eq('Street 1/2') 46 | expect(user.address.zipcode).to eq('12345') 47 | expect(user.address.city.name).to eq('NYC') 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/integration/extending_objects_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'I can extend objects' do 4 | before do 5 | module Examples 6 | class User; end 7 | 8 | class Admin; end 9 | end 10 | end 11 | 12 | specify 'defining attributes on an object' do 13 | attributes = { :name => 'John', :age => 29 } 14 | 15 | admin = Examples::Admin.new 16 | admin.extend(Virtus) 17 | 18 | admin.attribute :name, String 19 | admin.attribute :age, Integer 20 | 21 | admin.name = 'John' 22 | admin.age = 29 23 | 24 | expect(admin.name).to eql('John') 25 | expect(admin.age).to eql(29) 26 | 27 | expect(admin.attributes).to eql(attributes) 28 | 29 | new_attributes = { :name => 'Jane', :age => 28 } 30 | admin.attributes = new_attributes 31 | 32 | expect(admin.name).to eql('Jane') 33 | expect(admin.age).to eql(28) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/hash_attributes_coercion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | class Package 5 | include Virtus 6 | 7 | attribute :dimensions, Hash[Symbol => Float] 8 | attribute :meta_info , Hash[String => String] 9 | end 10 | 11 | 12 | describe Package do 13 | let(:instance) do 14 | described_class.new( 15 | :dimensions => { 'width' => "2.2", :height => 2, "length" => 4.5 }, 16 | :meta_info => { 'from' => :Me , :to => 'You' } 17 | ) 18 | end 19 | 20 | let(:dimensions) { instance.dimensions } 21 | let(:meta_info) { instance.meta_info } 22 | 23 | describe '#dimensions' do 24 | subject { dimensions } 25 | 26 | it 'has 3 keys' do 27 | expect(subject.keys.size).to eq(3) 28 | end 29 | it { is_expected.to have_key :width } 30 | it { is_expected.to have_key :height } 31 | it { is_expected.to have_key :length } 32 | 33 | it 'should be coerced to [Symbol => Float] format' do 34 | expect(dimensions[:width]).to be_eql(2.2) 35 | expect(dimensions[:height]).to be_eql(2.0) 36 | expect(dimensions[:length]).to be_eql(4.5) 37 | end 38 | end 39 | 40 | describe '#meta_info' do 41 | subject { meta_info } 42 | 43 | it 'has 2 keys' do 44 | expect(subject.keys.size).to eq(2) 45 | end 46 | it { is_expected.to have_key 'from' } 47 | it { is_expected.to have_key 'to' } 48 | 49 | it 'should be coerced to [String => String] format' do 50 | expect(meta_info['from']).to eq('Me') 51 | expect(meta_info['to']).to eq('You') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/integration/inheritance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Inheritance' do 4 | before do 5 | module Examples 6 | class Base 7 | include Virtus.model 8 | end 9 | 10 | class First < Base 11 | attribute :id, Fixnum 12 | attribute :name, String, default: ->(first, _) { "Named: #{first.id}" } 13 | attribute :description, String 14 | end 15 | 16 | class Second < Base 17 | attribute :something, String 18 | end 19 | end 20 | end 21 | 22 | it 'inherits model from the base class' do 23 | expect(Examples::First.attribute_set.map(&:name)).to eql([:id, :name, :description]) 24 | expect(Examples::Second.attribute_set.map(&:name)).to eql([:something]) 25 | end 26 | 27 | it 'sets correct attributes on the descendant classes' do 28 | first = Examples::First.new(:id => 1, :description => 'hello world') 29 | 30 | expect(first.id).to be(1) 31 | expect(first.name).to eql('Named: 1') 32 | expect(first.description).to eql('hello world') 33 | 34 | second = Examples::Second.new 35 | 36 | expect(second.something).to be(nil) 37 | 38 | second.something = 'foo bar' 39 | 40 | expect(second.something).to eql('foo bar') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/integration/injectible_coercers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'virtus' 2 | 3 | describe 'Injectible coercer' do 4 | before do 5 | module Examples 6 | class EmailAddress 7 | include Virtus.value_object 8 | 9 | values do 10 | attribute :address, String, :coercer => lambda { |add| add.downcase } 11 | end 12 | 13 | def self.coerce(input) 14 | if input.is_a?(String) 15 | new(:address => input) 16 | else 17 | new(input) 18 | end 19 | end 20 | end 21 | 22 | class User 23 | include Virtus.model 24 | 25 | attribute :email, EmailAddress, 26 | :coercer => lambda { |input| Examples::EmailAddress.coerce(input) } 27 | end 28 | end 29 | end 30 | 31 | after do 32 | Examples.send(:remove_const, :EmailAddress) 33 | Examples.send(:remove_const, :User) 34 | end 35 | 36 | let(:doe) { Examples::EmailAddress.new(:address => 'john.doe@example.com') } 37 | 38 | it 'accepts an email hash' do 39 | user = Examples::User.new :email => { :address => 'John.Doe@Example.Com' } 40 | expect(user.email).to eq(doe) 41 | end 42 | 43 | it 'coerces an embedded string' do 44 | user = Examples::User.new :email => 'John.Doe@Example.Com' 45 | expect(user.email).to eq(doe) 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /spec/integration/mass_assignment_with_accessors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "mass assignment with accessors" do 4 | 5 | before do 6 | module Examples 7 | class Product 8 | include Virtus 9 | 10 | attribute :id, Integer 11 | attribute :category, String 12 | attribute :subcategory, String 13 | 14 | def categories=(categories) 15 | self.category = categories.first 16 | self.subcategory = categories.last 17 | end 18 | 19 | private 20 | 21 | def _id=(value) 22 | self.id = value 23 | end 24 | end 25 | end 26 | end 27 | 28 | subject { Examples::Product.new(:categories => ['Office', 'Printers'], :_id => 100) } 29 | 30 | specify 'works uppon instantiation' do 31 | expect(subject.category).to eq('Office') 32 | expect(subject.subcategory).to eq('Printers') 33 | end 34 | 35 | specify 'can be set with #attributes=' do 36 | subject.attributes = {:categories => ['Home', 'Furniture']} 37 | expect(subject.category).to eq('Home') 38 | expect(subject.subcategory).to eq('Furniture') 39 | end 40 | 41 | specify 'respects accessor visibility' do 42 | expect(subject.id).not_to eq(100) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/integration/overriding_virtus_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'overriding virtus behavior' do 4 | 5 | before do 6 | module Examples 7 | class Article 8 | include Virtus 9 | 10 | attribute :title, String 11 | 12 | def title 13 | super || '' 14 | end 15 | 16 | def title=(name) 17 | super unless self.title == "can't be changed" 18 | end 19 | end 20 | end 21 | end 22 | 23 | describe 'overriding an attribute getter' do 24 | specify 'calls the defined getter' do 25 | expect(Examples::Article.new.title).to eq('') 26 | end 27 | 28 | specify 'super can be used to access the getter defined by virtus' do 29 | expect(Examples::Article.new(:title => 'example article').title).to eq('example article') 30 | end 31 | end 32 | 33 | describe 'overriding an attribute setter' do 34 | specify 'calls the defined setter' do 35 | article = Examples::Article.new(:title => "can't be changed") 36 | article.title = 'this will never be assigned' 37 | expect(article.title).to eq("can't be changed") 38 | end 39 | 40 | specify 'super can be used to access the setter defined by virtus' do 41 | article = Examples::Article.new(:title => 'example article') 42 | article.title = 'my new title' 43 | expect(article.title).to eq('my new title') 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/integration/required_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Using required attributes' do 4 | before do 5 | module Examples 6 | class User 7 | include Virtus.model(:strict => true) 8 | 9 | attribute :name, String 10 | attribute :age, Integer, :required => false 11 | end 12 | end 13 | end 14 | 15 | it 'raises coercion error when required attribute is nil' do 16 | expect { Examples::User.new(:name => nil) }.to raise_error(Virtus::CoercionError, "Failed to coerce attribute `name' from nil into String") 17 | end 18 | 19 | it 'does not raise coercion error when not required attribute is nil' do 20 | user = Examples::User.new(:name => 'Jane', :age => nil) 21 | 22 | expect(user.name).to eql('Jane') 23 | expect(user.age).to be(nil) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/integration/struct_as_embedded_value_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Using Struct as an embedded value attribute' do 4 | before do 5 | module Examples 6 | Point = Struct.new(:x, :y) 7 | 8 | class Rectangle 9 | include Virtus 10 | 11 | attribute :top_left, Point 12 | attribute :bottom_right, Point 13 | end 14 | end 15 | end 16 | 17 | subject do 18 | Examples::Rectangle.new(:top_left => [ 3, 5 ], :bottom_right => [ 8, 7 ]) 19 | end 20 | 21 | specify 'initialize a struct object with correct attributes' do 22 | expect(subject.top_left.x).to be(3) 23 | expect(subject.top_left.y).to be(5) 24 | 25 | expect(subject.bottom_right.x).to be(8) 26 | expect(subject.bottom_right.y).to be(7) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/integration/using_modules_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'I can define attributes within a module' do 4 | before do 5 | module Examples 6 | module Common 7 | include Virtus 8 | end 9 | 10 | module Name 11 | include Common 12 | 13 | attribute :name, String 14 | attribute :gamer, Boolean 15 | end 16 | 17 | module Age 18 | include Common 19 | 20 | attribute :age, Integer 21 | end 22 | 23 | class User 24 | include Name 25 | end 26 | 27 | class Admin < User 28 | include Age 29 | end 30 | 31 | class Moderator; end 32 | end 33 | end 34 | 35 | specify 'including a module with attributes into a class' do 36 | expect(Examples::User.attribute_set[:name]).to be_instance_of(Virtus::Attribute) 37 | expect(Examples::User.attribute_set[:gamer]).to be_instance_of(Virtus::Attribute::Boolean) 38 | 39 | expect(Examples::Admin.attribute_set[:name]).to be_instance_of(Virtus::Attribute) 40 | expect(Examples::Admin.attribute_set[:age]).to be_instance_of(Virtus::Attribute) 41 | 42 | user = Examples::Admin.new(:name => 'Piotr', :age => 29) 43 | expect(user.name).to eql('Piotr') 44 | expect(user.age).to eql(29) 45 | end 46 | 47 | specify 'including a module with attributes into an instance' do 48 | moderator = Examples::Moderator.new 49 | moderator.extend(Examples::Name, Examples::Age) 50 | 51 | moderator.attributes = { :name => 'John', :age => 21 } 52 | expect(moderator.name).to eql('John') 53 | expect(moderator.age).to eql(21) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/integration/value_object_with_custom_constructor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Defining a ValueObject with a custom constructor" do 4 | before do 5 | module Examples 6 | class Point 7 | include Virtus::ValueObject 8 | 9 | attribute :x, Integer 10 | attribute :y, Integer 11 | 12 | def initialize(attributes) 13 | if attributes.kind_of?(Array) 14 | self.x = attributes.first 15 | self.y = attributes.last 16 | else 17 | super 18 | end 19 | end 20 | end 21 | 22 | class Rectangle 23 | include Virtus 24 | 25 | attribute :top_left, Point 26 | attribute :bottom_right, Point 27 | end 28 | end 29 | end 30 | 31 | subject do 32 | Examples::Rectangle.new(:top_left => [ 3, 4 ], :bottom_right => [ 5, 8 ]) 33 | end 34 | 35 | specify "initialize a value object attribute with correct attributes" do 36 | expect(subject.top_left.x).to be(3) 37 | expect(subject.top_left.y).to be(4) 38 | 39 | expect(subject.bottom_right.x).to be(5) 40 | expect(subject.bottom_right.y).to be(8) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/integration/virtus/instance_level_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, 'instance level attributes' do 4 | subject do 5 | subject = Object.new 6 | subject.singleton_class.send(:include, Virtus) 7 | subject 8 | end 9 | 10 | let(:attribute) { subject.singleton_class.attribute(:name, String) } 11 | 12 | before do 13 | pending if RUBY_VERSION < '1.9' 14 | end 15 | 16 | context 'adding an attribute' do 17 | it 'allows setting the attribute value on the instance' do 18 | attribute 19 | subject.name = 'foo' 20 | expect(subject.name).to eql('foo') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/integration/virtus/value_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::ValueObject do 4 | let(:class_under_test) do 5 | Class.new do 6 | def self.name 7 | 'GeoLocation' 8 | end 9 | 10 | include Virtus::ValueObject 11 | 12 | attribute :latitude, Float 13 | attribute :longitude, Float 14 | end 15 | end 16 | 17 | let(:attribute_values) { { :latitude => 10.0, :longitude => 20.0 } } 18 | 19 | let(:instance_with_equal_state) { class_under_test.new(attribute_values) } 20 | 21 | let(:instance_with_different_state) do 22 | class_under_test.new(:latitude => attribute_values[:latitude]) 23 | end 24 | 25 | subject { class_under_test.new(attribute_values) } 26 | 27 | describe 'initialization' do 28 | it 'sets the attribute values provided to Class.new' do 29 | expect(class_under_test.new(:latitude => 10000.001).latitude).to eq(10000.001) 30 | expect(subject.latitude).to eql(attribute_values[:latitude]) 31 | end 32 | end 33 | 34 | describe 'writer visibility' do 35 | it 'attributes are configured for private writers' do 36 | expect(class_under_test.attribute_set[:latitude].public_reader?).to be(true) 37 | expect(class_under_test.attribute_set[:longitude].public_writer?).to be(false) 38 | end 39 | 40 | it 'writer methods are set to private' do 41 | private_methods = class_under_test.private_instance_methods 42 | private_methods.map! { |m| m.to_s } 43 | expect(private_methods).to include('latitude=', 'longitude=', 'attributes=') 44 | end 45 | 46 | it 'attempts to call attribute writer methods raises NameError' do 47 | expect { subject.latitude = 5.0 }.to raise_exception(NameError) 48 | expect { subject.longitude = 5.0 }.to raise_exception(NameError) 49 | end 50 | end 51 | 52 | describe 'equality' do 53 | describe '#==' do 54 | it 'returns true for different objects with the same state' do 55 | expect(subject).to eq(instance_with_equal_state) 56 | end 57 | 58 | it 'returns false for different objects with different state' do 59 | expect(subject).not_to eq(instance_with_different_state) 60 | end 61 | end 62 | 63 | describe '#eql?' do 64 | it 'returns true for different objects with the same state' do 65 | expect(subject).to eql(instance_with_equal_state) 66 | end 67 | 68 | it 'returns false for different objects with different state' do 69 | expect(subject).not_to eql(instance_with_different_state) 70 | end 71 | end 72 | 73 | describe '#equal?' do 74 | it 'returns false for different objects with the same state' do 75 | expect(subject).not_to equal(instance_with_equal_state) 76 | end 77 | 78 | it 'returns false for different objects with different state' do 79 | expect(subject).not_to equal(instance_with_different_state) 80 | end 81 | end 82 | 83 | describe '#hash' do 84 | it 'returns the same value for different objects with the same state' do 85 | expect(subject.hash).to eql(instance_with_equal_state.hash) 86 | end 87 | 88 | it 'returns different values for different objects with different state' do 89 | expect(subject.hash).not_to eql(instance_with_different_state.hash) 90 | end 91 | end 92 | end 93 | 94 | describe '#inspect' do 95 | it 'includes the class name and attribute values' do 96 | expect(subject.inspect).to eq('#') 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/shared/constants_helpers.rb: -------------------------------------------------------------------------------- 1 | module ConstantsHelpers 2 | 3 | extend self 4 | 5 | # helper to remove constants after test-runs. 6 | def undef_constant(mod, constant_name) 7 | mod.send(:remove_const, constant_name) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/shared/freeze_method_behavior.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'a #freeze method' do 2 | let(:sample_exception) do 3 | begin 4 | object.dup.freeze.instance_variable_set(:@foo, :bar) 5 | rescue => exception 6 | exception 7 | end 8 | end 9 | 10 | let(:expected_exception_class) do 11 | # Ruby 1.8 blows up with TypeError Ruby 1.9 with RuntimeError 12 | sample_exception.class 13 | end 14 | 15 | let(:expected_exception_message) do 16 | # Ruby 1.8 blows up with a different message than Ruby 1.9 17 | sample_exception.message 18 | end 19 | 20 | it_should_behave_like 'an idempotent method' 21 | 22 | it 'returns object' do 23 | is_expected.to be(object) 24 | end 25 | 26 | it 'prevents future modifications' do 27 | subject 28 | expectation = raise_error(expected_exception_class,expected_exception_message) 29 | expect { object.instance_variable_set(:@foo, :bar) }.to(expectation) 30 | end 31 | 32 | describe '#frozen?' do 33 | subject { super().frozen? } 34 | it { is_expected.to be(true) } 35 | end 36 | 37 | it 'allows to access attribute' do 38 | expect(subject.name).to eql('John') 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/shared/idempotent_method_behaviour.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'an idempotent method' do 2 | it 'is idempotent' do 3 | is_expected.to equal(subject) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/shared/options_class_method.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for 'an options class method' do 2 | context 'with no argument' do 3 | subject { object.send(method) } 4 | 5 | it { is_expected.to be(default) } 6 | end 7 | 8 | context 'with a default value' do 9 | subject { object.send(method, value) } 10 | 11 | let(:value) { mock('value') } 12 | 13 | it { is_expected.to equal(object) } 14 | 15 | it 'sets the default value for the class method' do 16 | expect { subject }.to change { object.send(method) }.from(default).to(value) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if RUBY_ENGINE == 'ruby' && RUBY_VERSION >= '3.0' 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | require 'rspec' 7 | require 'bogus/rspec' 8 | require 'virtus' 9 | 10 | module Virtus 11 | def self.warn(*) 12 | # shut up in tests 13 | end 14 | end 15 | 16 | ENV['TZ'] = 'UTC' 17 | 18 | # require spec support files and shared behavior 19 | Dir[File.expand_path('../shared/**/*.rb', __FILE__)].each { |file| require file } 20 | 21 | RSpec.configure do |config| 22 | # Remove anonymous- and example- Attribute classes from Attribute descendants 23 | config.after :all do 24 | stack = [ Virtus::Attribute ] 25 | while klass = stack.pop 26 | klass.descendants.delete_if do |descendant| 27 | descendant.name.nil? || descendant.name.empty? || descendant.name.start_with?('Examples::') 28 | end 29 | stack.concat(klass.descendants) 30 | end 31 | end 32 | 33 | # Remove constants in the Example-Module 34 | config.after :each do 35 | if defined?(Examples) 36 | Examples.constants.each do |const_name| 37 | ConstantsHelpers.undef_constant(Examples, const_name) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/boolean/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Boolean, '#coerce' do 4 | subject { object.coerce(input) } 5 | 6 | let(:object) { described_class.build('Boolean', options) } 7 | let(:options) { {} } 8 | 9 | context 'when strict is turned off' do 10 | context 'with a truthy value' do 11 | let(:input) { 1 } 12 | 13 | it { is_expected.to be(true) } 14 | end 15 | 16 | context 'with a falsy value' do 17 | let(:input) { 0 } 18 | 19 | it { is_expected.to be(false) } 20 | end 21 | end 22 | 23 | context 'when strict is turned on' do 24 | let(:options) { { :strict => true } } 25 | 26 | context 'with a coercible input' do 27 | let(:input) { 1 } 28 | 29 | it { is_expected.to be(true) } 30 | end 31 | 32 | context 'with a non-coercible input' do 33 | let(:input) { 'no idea if true or false' } 34 | 35 | it 'raises coercion error' do 36 | expect { subject }.to raise_error( 37 | Virtus::CoercionError, 38 | /Failed to coerce "no idea if true or false"/ 39 | ) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Boolean, '#value_coerced?' do 4 | subject { object.value_coerced?(input) } 5 | 6 | let(:object) { described_class.build('Boolean') } 7 | 8 | context 'when input is true' do 9 | let(:input) { true } 10 | 11 | it { is_expected.to be(true) } 12 | end 13 | 14 | context 'when input is false' do 15 | let(:input) { false } 16 | 17 | it { is_expected.to be(true) } 18 | end 19 | 20 | context 'when input is not coerced' do 21 | let(:input) { 1 } 22 | 23 | it { is_expected.to be(false) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/class_methods/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '.build' do 4 | subject { described_class.build(type, options.merge(:name => name)) } 5 | 6 | let(:name) { :test } 7 | let(:type) { String } 8 | let(:options) { {} } 9 | 10 | shared_examples_for 'a valid attribute instance' do 11 | it { is_expected.to be_instance_of(Virtus::Attribute) } 12 | 13 | it { is_expected.to be_frozen } 14 | end 15 | 16 | context 'without options' do 17 | it_behaves_like 'a valid attribute instance' 18 | 19 | it { is_expected.to be_coercible } 20 | it { is_expected.to be_public_reader } 21 | it { is_expected.to be_public_writer } 22 | it { is_expected.not_to be_lazy } 23 | 24 | it 'sets up a coercer' do 25 | expect(subject.options[:coerce]).to be(true) 26 | expect(subject.coercer).to be_instance_of(Virtus::Attribute::Coercer) 27 | end 28 | end 29 | 30 | context 'when name is passed as a string' do 31 | let(:name) { 'something' } 32 | 33 | describe '#name' do 34 | subject { super().name } 35 | it { is_expected.to be(:something) } 36 | end 37 | end 38 | 39 | context 'when coercion is turned off in options' do 40 | let(:options) { { :coerce => false } } 41 | 42 | it_behaves_like 'a valid attribute instance' 43 | 44 | it { is_expected.not_to be_coercible } 45 | end 46 | 47 | context 'when options specify reader visibility' do 48 | let(:options) { { :reader => :private } } 49 | 50 | it_behaves_like 'a valid attribute instance' 51 | 52 | it { is_expected.not_to be_public_reader } 53 | it { is_expected.to be_public_writer } 54 | end 55 | 56 | context 'when options specify writer visibility' do 57 | let(:options) { { :writer => :private } } 58 | 59 | it_behaves_like 'a valid attribute instance' 60 | 61 | it { is_expected.to be_public_reader } 62 | it { is_expected.not_to be_public_writer } 63 | end 64 | 65 | context 'when options specify lazy accessor' do 66 | let(:options) { { :lazy => true } } 67 | 68 | it_behaves_like 'a valid attribute instance' 69 | 70 | it { is_expected.to be_lazy } 71 | end 72 | 73 | context 'when options specify strict mode' do 74 | let(:options) { { :strict => true } } 75 | 76 | it_behaves_like 'a valid attribute instance' 77 | 78 | it { is_expected.to be_strict } 79 | end 80 | 81 | context 'when options specify nullify blank mode' do 82 | let(:options) { { :nullify_blank => true } } 83 | 84 | it_behaves_like 'a valid attribute instance' 85 | 86 | it { is_expected.to be_nullify_blank } 87 | end 88 | 89 | context 'when type is a string' do 90 | let(:type) { 'Integer' } 91 | 92 | it_behaves_like 'a valid attribute instance' 93 | 94 | describe '#type' do 95 | subject { super().type } 96 | it { is_expected.to be(Axiom::Types::Integer) } 97 | end 98 | end 99 | 100 | context 'when type is a range' do 101 | let(:type) { 0..10 } 102 | 103 | it_behaves_like 'a valid attribute instance' 104 | 105 | describe '#type' do 106 | subject { super().type } 107 | it { is_expected.to be(Axiom::Types.infer(Range)) } 108 | end 109 | end 110 | 111 | context 'when type is a symbol of an existing class constant' do 112 | let(:type) { :String } 113 | 114 | it_behaves_like 'a valid attribute instance' 115 | 116 | describe '#type' do 117 | subject { super().type } 118 | it { is_expected.to be(Axiom::Types::String) } 119 | end 120 | end 121 | 122 | context 'when type is an axiom type' do 123 | let(:type) { Axiom::Types::Integer } 124 | 125 | it_behaves_like 'a valid attribute instance' 126 | 127 | describe '#type' do 128 | subject { super().type } 129 | it { is_expected.to be(type) } 130 | end 131 | end 132 | 133 | context 'when custom attribute class exists for a given primitive' do 134 | let(:type) { Class.new } 135 | let(:attribute) { Class.new(Virtus::Attribute) } 136 | 137 | before do 138 | attribute.primitive(type) 139 | end 140 | 141 | it { is_expected.to be_instance_of(attribute) } 142 | 143 | describe '#type' do 144 | subject { super().type } 145 | it { is_expected.to be(Axiom::Types::Object) } 146 | end 147 | end 148 | 149 | context 'when custom attribute class exists for a given array with member coercion defined' do 150 | let(:type) { Class.new(Array)[String] } 151 | let(:attribute) { Class.new(Virtus::Attribute) } 152 | 153 | before do 154 | attribute.primitive(type.class) 155 | end 156 | 157 | it { is_expected.to be_instance_of(attribute) } 158 | 159 | describe '#type' do 160 | subject { super().type } 161 | it { is_expected.to be < Axiom::Types::Collection } 162 | end 163 | end 164 | 165 | context 'when custom collection-like attribute class exists for a given enumerable primitive' do 166 | let(:type) { Class.new { include Enumerable } } 167 | let(:attribute) { Class.new(Virtus::Attribute::Collection) } 168 | 169 | before do 170 | attribute.primitive(type) 171 | end 172 | 173 | it { is_expected.to be_instance_of(attribute) } 174 | 175 | describe '#type' do 176 | subject { super().type } 177 | it { is_expected.to be < Axiom::Types::Collection } 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/class_methods/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '.coerce' do 4 | subject { described_class.coerce } 5 | 6 | after :all do 7 | described_class.coerce(true) 8 | end 9 | 10 | context 'with a value' do 11 | it 'sets the value and return self' do 12 | expect(described_class.coerce(false)).to be(described_class) 13 | expect(subject).to be(false) 14 | end 15 | end 16 | 17 | context 'when it is set to true' do 18 | before do 19 | described_class.coerce(true) 20 | end 21 | 22 | it { is_expected.to be(true) } 23 | end 24 | 25 | context 'when it is set to false' do 26 | before do 27 | described_class.coerce(false) 28 | end 29 | 30 | it { is_expected.to be(false) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#coerce' do 4 | subject { object.coerce(input) } 5 | 6 | fake(:coercer) { Virtus::Attribute::Coercer } 7 | 8 | let(:object) { 9 | described_class.build(String, 10 | :coercer => coercer, :strict => strict, :required => required, :nullify_blank => nullify_blank) 11 | } 12 | 13 | let(:required) { true } 14 | let(:nullify_blank) { false } 15 | let(:input) { 1 } 16 | let(:output) { '1' } 17 | 18 | context 'when strict mode is turned off' do 19 | let(:strict) { false } 20 | 21 | it 'uses coercer to coerce the input value' do 22 | mock(coercer).call(input) { output } 23 | 24 | expect(subject).to be(output) 25 | 26 | expect(coercer).to have_received.call(input) 27 | end 28 | end 29 | 30 | context 'when strict mode is turned on' do 31 | let(:strict) { true } 32 | 33 | it 'uses coercer to coerce the input value' do 34 | mock(coercer).call(input) { output } 35 | mock(coercer).success?(String, output) { true } 36 | 37 | expect(subject).to be(output) 38 | 39 | expect(coercer).to have_received.call(input) 40 | expect(coercer).to have_received.success?(String, output) 41 | end 42 | 43 | context 'when attribute is not required and input is nil' do 44 | let(:required) { false } 45 | let(:input) { nil } 46 | 47 | it 'returns nil' do 48 | mock(coercer).call(input) { input } 49 | mock(coercer).success?(String, input) { false } 50 | 51 | expect(subject).to be(nil) 52 | 53 | expect(coercer).to have_received.call(input) 54 | expect(coercer).to have_received.success?(String, input) 55 | end 56 | end 57 | 58 | context 'when attribute is required and input is nil' do 59 | let(:input) { nil } 60 | 61 | it 'returns raises error' do 62 | mock(coercer).call(input) { input } 63 | mock(coercer).success?(String, input) { false } 64 | 65 | expect { subject }.to raise_error(Virtus::CoercionError) 66 | 67 | expect(coercer).to have_received.call(input) 68 | expect(coercer).to have_received.success?(String, input) 69 | end 70 | end 71 | 72 | it 'raises error when input was not coerced' do 73 | mock(coercer).call(input) { input } 74 | mock(coercer).success?(String, input) { false } 75 | 76 | expect { subject }.to raise_error(Virtus::CoercionError) 77 | 78 | expect(coercer).to have_received.call(input) 79 | expect(coercer).to have_received.success?(String, input) 80 | end 81 | end 82 | 83 | context 'when the input is an empty String' do 84 | let(:input) { '' } 85 | let(:output) { '' } 86 | 87 | context 'when nullify_blank is turned on' do 88 | let(:nullify_blank) { true } 89 | let(:strict) { false } 90 | let(:require) { false } 91 | 92 | it 'returns nil' do 93 | mock(coercer).call(input) { input } 94 | mock(coercer).success?(String, input) { false } 95 | 96 | expect(subject).to be_nil 97 | 98 | expect(coercer).to have_received.call(input) 99 | expect(coercer).to have_received.success?(String, input) 100 | end 101 | 102 | it 'returns the ouput if it was coerced' do 103 | mock(coercer).call(input) { output } 104 | mock(coercer).success?(String, output) { true } 105 | 106 | expect(subject).to be(output) 107 | 108 | expect(coercer).to have_received.call(input) 109 | expect(coercer).to have_received.success?(String, output) 110 | end 111 | end 112 | 113 | context 'when both nullify_blank and strict are turned on' do 114 | let(:nullify_blank) { true } 115 | let(:strict) { true } 116 | 117 | it 'does not raises an coercion error' do 118 | mock(coercer).call(input) { input } 119 | mock(coercer).success?(String, input) { false } 120 | 121 | expect { subject }.not_to raise_error 122 | expect(subject).to be_nil 123 | 124 | expect(coercer).to have_received.call(input) 125 | expect(coercer).to have_received.success?(String, input) 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/coercible_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#coercible?' do 4 | subject { object.coercible? } 5 | 6 | let(:object) { described_class.build(String, options) } 7 | let(:options) { Hash[:coerce => coerce] } 8 | 9 | context 'when :coerce is set to true' do 10 | let(:coerce) { true } 11 | 12 | it { is_expected.to be(true) } 13 | end 14 | 15 | context 'when :coerce is set to false' do 16 | let(:coerce) { false } 17 | 18 | it { is_expected.to be(false) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/collection/class_methods/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '.build' do 4 | subject { described_class.build(type, options) } 5 | 6 | let(:options) { {} } 7 | 8 | shared_examples_for 'a valid collection attribute instance' do 9 | it { is_expected.to be_instance_of(Virtus::Attribute::Collection) } 10 | 11 | it { is_expected.to be_frozen } 12 | end 13 | 14 | context 'when type is Array' do 15 | let(:type) { Array } 16 | 17 | it_behaves_like 'a valid collection attribute instance' 18 | 19 | it 'sets default member type' do 20 | expect(subject.type.member_type).to be(Axiom::Types::Object) 21 | end 22 | end 23 | 24 | context 'when type is Array[Virtus::Attribute::Boolean]' do 25 | let(:type) { Array[Virtus::Attribute::Boolean] } 26 | 27 | it_behaves_like 'a valid collection attribute instance' 28 | 29 | it 'sets member type' do 30 | expect(subject.type.member_type).to be(Axiom::Types::Boolean) 31 | end 32 | end 33 | 34 | context 'when type is Array[Float]' do 35 | let(:type) { Array[Float] } 36 | 37 | it_behaves_like 'a valid collection attribute instance' 38 | 39 | it 'sets member type' do 40 | expect(subject.type.member_type).to be(Axiom::Types::Float) 41 | end 42 | end 43 | 44 | context 'when type is Array[String, Integer]' do 45 | let(:type) { Array[String, Integer] } 46 | 47 | specify do 48 | expect { subject }.to raise_error( 49 | NotImplementedError, 50 | "build SumType from list of types (#{type.inspect})" 51 | ) 52 | end 53 | end 54 | 55 | context 'when type is Set' do 56 | let(:type) { Set } 57 | 58 | it_behaves_like 'a valid collection attribute instance' 59 | 60 | it 'sets default member type' do 61 | expect(subject.type.member_type).to be(Axiom::Types::Object) 62 | end 63 | end 64 | 65 | context 'when type is Set[Float]' do 66 | let(:type) { Set[Float] } 67 | 68 | it_behaves_like 'a valid collection attribute instance' 69 | 70 | it 'sets member type' do 71 | expect(subject.type.member_type).to be(Axiom::Types::Float) 72 | end 73 | end 74 | 75 | context 'when type is an Enumerable' do 76 | let(:type) { Class.new { include Enumerable } } 77 | 78 | it_behaves_like 'a valid collection attribute instance' 79 | end 80 | 81 | context 'when type is Array subclass' do 82 | let(:type) { Class.new(Array) } 83 | 84 | it_behaves_like 'a valid collection attribute instance' 85 | end 86 | 87 | context 'when type is a custom collection instance' do 88 | let(:type) { Class.new(Array)[String] } 89 | 90 | it_behaves_like 'a valid collection attribute instance' 91 | 92 | it 'sets member type' do 93 | expect(subject.type.member_type).to be(Axiom::Types::String) 94 | end 95 | end 96 | 97 | context 'when strict mode is used' do 98 | let(:type) { Array[String] } 99 | let(:options) { { strict: true } } 100 | 101 | it 'sets strict mode for member type' do 102 | expect(subject.member_type).to be_strict 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/collection/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Collection, '#coerce' do 4 | subject { object.coerce(input) } 5 | 6 | context 'when input is an array' do 7 | context 'when member type is a primitive' do 8 | fake(:coercer) { Virtus::Attribute::Coercer } 9 | fake(:member_type) { Virtus::Attribute } 10 | 11 | let(:member_primitive) { Integer } 12 | let(:input) { ['1', '2'] } 13 | 14 | let(:object) { 15 | described_class.build(Array[member_primitive], :coercer => coercer, :member_type => member_type) 16 | } 17 | 18 | it 'uses coercer to coerce members' do 19 | mock(coercer).call(input) { input } 20 | mock(member_type).finalize { member_type } 21 | mock(member_type).coerce('1') { 1 } 22 | mock(member_type).coerce('2') { 2 } 23 | 24 | expect(subject).to eq([1, 2]) 25 | 26 | expect(member_type).to have_received.coerce('1') 27 | expect(member_type).to have_received.coerce('2') 28 | end 29 | end 30 | 31 | context 'when member type is an EV' do 32 | let(:member_primitive) { Struct.new(:id) } 33 | let(:input) { [1, 2] } 34 | let(:object) { described_class.build(Array[member_primitive]) } 35 | 36 | it 'coerces members' do 37 | expect(subject).to eq([member_primitive.new(1), member_primitive.new(2)]) 38 | end 39 | end 40 | 41 | context 'when member type is a hash with key/value coercion' do 42 | let(:member_primitive) { Hash[String => Integer] } 43 | let(:member_attribute) { Virtus::Attribute.build(member_primitive) } 44 | let(:input) { [{:one => '1'}, {:two => '2'}] } 45 | let(:output) { [member_attribute.coerce(input.first), member_attribute.coerce(input.last)] } 46 | let(:object) { described_class.build(Array[member_primitive]) } 47 | 48 | it 'coerces members' do 49 | expect(subject).to eq(output) 50 | end 51 | end 52 | end 53 | 54 | context 'when input is nil' do 55 | let(:input) { nil } 56 | 57 | fake(:coercer) { Virtus::Attribute::Coercer } 58 | fake(:member_type) { Virtus::Attribute } 59 | 60 | let(:member_primitive) { Integer } 61 | 62 | let(:object) { 63 | described_class.build( 64 | Array[member_primitive], coercer: coercer, member_type: member_type 65 | ) 66 | } 67 | 68 | it 'returns nil' do 69 | mock(coercer).call(input) { input } 70 | 71 | expect(subject).to be(input) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'set' 3 | 4 | describe Virtus::Attribute::Collection, '#value_coerced?' do 5 | subject { object.value_coerced?(input) } 6 | 7 | let(:object) { described_class.build(Array[Integer]) } 8 | 9 | context 'when input has correctly typed members' do 10 | let(:input) { [1, 2, 3] } 11 | 12 | it { is_expected.to be(true) } 13 | end 14 | 15 | context 'when input has incorrectly typed members' do 16 | let(:input) { [1, 2, '3'] } 17 | 18 | it { is_expected.to be(false) } 19 | end 20 | 21 | context 'when the collection type is incorrect' do 22 | let(:input) { Set[1, 2, 3] } 23 | 24 | it { is_expected.to be(false) } 25 | end 26 | 27 | context 'when the input is empty' do 28 | let(:input) { [] } 29 | it { is_expected.to be(true) } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/comparison_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#== (defined by including Virtus::Equalizer)' do 4 | let(:attribute) { described_class.build(String, :name => :name) } 5 | 6 | it 'returns true when attributes have same type and options' do 7 | equal_attribute = described_class.build(String, :name => :name) 8 | expect(attribute == equal_attribute).to be_truthy 9 | end 10 | 11 | it 'returns false when attributes have different type' do 12 | different_attribute = described_class.build(Integer, :name => :name) 13 | expect(attribute == different_attribute).to be_falsey 14 | end 15 | 16 | it 'returns false when attributes have different options' do 17 | different_attribute = described_class.build(Integer, :name => :name_two) 18 | expect(attribute == different_attribute).to be_falsey 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/custom_collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Collection, 'custom subclass' do 4 | subject { attribute_class.build(primitive) } 5 | 6 | let(:primitive) { Class.new { include Enumerable } } 7 | 8 | after do 9 | described_class.descendants.delete(attribute_class) 10 | end 11 | 12 | context 'when primitive is set on the attribute subclass' do 13 | let(:attribute_class) { Class.new(described_class).primitive(primitive) } 14 | 15 | describe '#primitive' do 16 | subject { super().primitive } 17 | it { is_expected.to be(primitive) } 18 | end 19 | end 20 | 21 | context 'when primitive is not set on the attribute subclass' do 22 | let(:attribute_class) { Class.new(described_class) } 23 | 24 | describe '#primitive' do 25 | subject { super().primitive } 26 | it { is_expected.to be(primitive) } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/defined_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#defined?' do 4 | subject { object.defined?(instance) } 5 | 6 | let(:object) { described_class.build(String, :name => name) } 7 | 8 | let(:model) { Class.new { attr_accessor :test } } 9 | let(:name) { :test } 10 | let(:instance) { model.new } 11 | 12 | context 'when the attribute value has not been defined' do 13 | it { is_expected.to be(false) } 14 | end 15 | 16 | context 'when the attribute value has been defined' do 17 | before { instance.test = nil } 18 | it { is_expected.to be(true) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::EmbeddedValue, '.build' do 4 | subject { described_class.build(type) } 5 | 6 | context 'when type is a Virtus.model' do 7 | let(:type) { Class.new { include Virtus.model } } 8 | 9 | it { is_expected.to be_frozen } 10 | 11 | it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } 12 | 13 | describe '#coercer' do 14 | subject { super().coercer } 15 | it { is_expected.to be_instance_of(described_class::FromOpenStruct) } 16 | end 17 | end 18 | 19 | context 'when type includes Virtus' do 20 | let(:type) { Class.new { include Virtus } } 21 | 22 | it { is_expected.to be_frozen } 23 | 24 | it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } 25 | 26 | describe '#coercer' do 27 | subject { super().coercer } 28 | it { is_expected.to be_instance_of(described_class::FromOpenStruct) } 29 | end 30 | end 31 | 32 | context 'when type is an OpenStruct subclass' do 33 | let(:type) { Class.new(OpenStruct) } 34 | 35 | it { is_expected.to be_frozen } 36 | 37 | it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } 38 | 39 | describe '#coercer' do 40 | subject { super().coercer } 41 | it { is_expected.to be_instance_of(described_class::FromOpenStruct) } 42 | end 43 | end 44 | 45 | context 'when type is OpenStruct' do 46 | let(:type) { OpenStruct } 47 | 48 | it { is_expected.to be_frozen } 49 | 50 | it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } 51 | 52 | describe '#coercer' do 53 | subject { super().coercer } 54 | it { is_expected.to be_instance_of(described_class::FromOpenStruct) } 55 | end 56 | end 57 | 58 | context 'when type is Struct' do 59 | let(:type) { Struct.new(:test) } 60 | 61 | it { is_expected.to be_frozen } 62 | 63 | it { is_expected.to be_instance_of(Virtus::Attribute::EmbeddedValue) } 64 | 65 | describe '#coercer' do 66 | subject { super().coercer } 67 | it { is_expected.to be_instance_of(described_class::FromStruct) } 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/embedded_value/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::EmbeddedValue, '#coerce' do 4 | subject { object.coerce(input) } 5 | 6 | let(:object) { described_class.build(model, options) } 7 | let(:options) { {} } 8 | 9 | context 'when primitive is OpenStruct' do 10 | let(:model) { OpenStruct } 11 | 12 | context 'when input is an attribute hash' do 13 | let(:input) { Hash[name: 'Piotr', age: 30] } 14 | 15 | it { is_expected.to be_instance_of(model) } 16 | 17 | describe '#name' do 18 | subject { super().name } 19 | it { is_expected.to eql('Piotr') } 20 | end 21 | 22 | describe '#age' do 23 | subject { super().age } 24 | it { is_expected.to eql(30) } 25 | end 26 | end 27 | 28 | context 'when input is nil' do 29 | let(:input) { nil } 30 | 31 | it { is_expected.to be(nil) } 32 | end 33 | 34 | context 'when input is a model instance' do 35 | let(:input) { OpenStruct.new } 36 | 37 | it { is_expected.to be(input) } 38 | end 39 | end 40 | 41 | context 'when primitive is Struct' do 42 | let(:model) { Struct.new(:name, :age) } 43 | 44 | context 'when input is an attribute hash' do 45 | let(:input) { ['Piotr', 30] } 46 | 47 | it { is_expected.to be_instance_of(model) } 48 | 49 | describe '#name' do 50 | subject { super().name } 51 | it { is_expected.to eql('Piotr') } 52 | end 53 | 54 | describe '#age' do 55 | subject { super().age } 56 | it { is_expected.to eql(30) } 57 | end 58 | end 59 | 60 | context 'when input is nil' do 61 | let(:input) { nil } 62 | 63 | it { is_expected.to be(nil) } 64 | end 65 | 66 | context 'when input is a model instance' do 67 | let(:input) { model.new('Piotr', 30) } 68 | 69 | it { is_expected.to be(input) } 70 | end 71 | end 72 | 73 | context 'when :strict mode is enabled' do 74 | let(:model) { Struct.new(:name) } 75 | let(:options) { { :strict => true } } 76 | 77 | context 'when input is coercible' do 78 | let(:input) { ['Piotr'] } 79 | 80 | it { is_expected.to eql(model.new('Piotr')) } 81 | end 82 | 83 | context 'when input is not coercible' do 84 | let(:input) { nil } 85 | 86 | it 'raises error' do 87 | expect { subject }.to raise_error(Virtus::CoercionError) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/get_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#get' do 4 | subject { object.get(instance) } 5 | 6 | let(:object) { described_class.build(String, options.update(:name => name)) } 7 | 8 | let(:model) { Class.new { attr_accessor :test } } 9 | let(:name) { :test } 10 | let(:instance) { model.new } 11 | let(:value) { 'Jane Doe' } 12 | let(:options) { {} } 13 | 14 | context 'with :lazy is set to false' do 15 | before do 16 | instance.test = value 17 | end 18 | 19 | it { is_expected.to be(value) } 20 | end 21 | 22 | context 'with :lazy is set to true' do 23 | let(:options) { { :lazy => true, :default => value } } 24 | 25 | it { is_expected.to eql(value) } 26 | 27 | it 'sets default only on first access' do 28 | expect(object.get(instance)).to eql(value) 29 | expect(object.get(instance)).to be(instance.test) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/hash/class_methods/build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Hash, '.build' do 4 | subject { described_class.build(type, options) } 5 | 6 | let(:options) { {} } 7 | 8 | shared_examples_for 'a valid hash attribute instance' do 9 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 10 | 11 | it { is_expected.to be_frozen } 12 | end 13 | 14 | context 'when type is Hash' do 15 | let(:type) { Hash } 16 | 17 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 18 | 19 | it 'sets default key type' do 20 | expect(subject.type.key_type).to be(Axiom::Types::Object) 21 | end 22 | 23 | it 'sets default value type' do 24 | expect(subject.type.value_type).to be(Axiom::Types::Object) 25 | end 26 | end 27 | 28 | context 'when type is Hash[String => Integer]' do 29 | let(:type) { Hash[String => Integer] } 30 | 31 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 32 | 33 | it 'sets key type' do 34 | expect(subject.type.key_type).to be(Axiom::Types::String) 35 | end 36 | 37 | it 'sets value type' do 38 | expect(subject.type.value_type).to be(Axiom::Types::Integer) 39 | end 40 | end 41 | 42 | context 'when type is Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean]' do 43 | let(:type) { Hash[Virtus::Attribute::Hash => Virtus::Attribute::Boolean] } 44 | 45 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 46 | 47 | it 'sets key type' do 48 | expect(subject.type.key_type).to be(Axiom::Types::Hash) 49 | end 50 | 51 | it 'sets value type' do 52 | expect(subject.type.value_type).to be(Axiom::Types::Boolean) 53 | end 54 | end 55 | 56 | context 'when type is Hash[Struct.new(:id) => Integer]' do 57 | let(:type) { Hash[key_type => Integer] } 58 | let(:key_type) { Struct.new(:id) } 59 | 60 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 61 | 62 | it 'sets key type' do 63 | expect(subject.type.key_type).to be(key_type) 64 | end 65 | 66 | it 'sets value type' do 67 | expect(subject.type.value_type).to be(Axiom::Types::Integer) 68 | end 69 | end 70 | 71 | context 'when type is Hash[String => Struct.new(:id)]' do 72 | let(:type) { Hash[String => value_type] } 73 | let(:value_type) { Struct.new(:id) } 74 | 75 | it { is_expected.to be_instance_of(Virtus::Attribute::Hash) } 76 | 77 | it 'sets key type' do 78 | expect(subject.type.key_type).to be(Axiom::Types::String) 79 | end 80 | 81 | it 'sets value type' do 82 | expect(subject.type.value_type).to be(value_type) 83 | end 84 | end 85 | 86 | context 'when type is Hash[String => Integer, Integer => String]' do 87 | let(:type) { Hash[String => Integer, :Integer => :String] } 88 | 89 | specify do 90 | expect { subject }.to raise_error( 91 | ArgumentError, 92 | "more than one [key => value] pair in `#{type}`" 93 | ) 94 | end 95 | end 96 | 97 | context 'when strict mode is used' do 98 | let(:type) { Hash[String => Integer] } 99 | let(:options) { { :strict => true } } 100 | 101 | it 'sets the strict mode for key/value types' do 102 | expect(subject.key_type).to be_strict 103 | expect(subject.value_type).to be_strict 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/hash/coerce_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute::Hash, '#coerce' do 4 | subject { object.coerce(input) } 5 | 6 | fake(:coercer) { Virtus::Attribute::Coercer } 7 | fake(:key_type) { Virtus::Attribute } 8 | fake(:value_type) { Virtus::Attribute } 9 | 10 | let(:object) { 11 | described_class.build(Hash[key_primitive => value_primitive], options) 12 | } 13 | 14 | let(:options) { {} } 15 | 16 | context 'when input is coercible to hash' do 17 | let(:input) { Class.new { def to_hash; { :hello => 'World' }; end }.new } 18 | let(:object) { described_class.build(Hash) } 19 | 20 | it { is_expected.to eq(:hello => 'World') } 21 | end 22 | 23 | context 'when input is not coercible to hash' do 24 | let(:input) { 'not really a hash' } 25 | let(:object) { described_class.build(Hash) } 26 | 27 | it { is_expected.to be(input) } 28 | end 29 | 30 | context 'when input is a hash' do 31 | context 'when key/value types are primitives' do 32 | let(:options) { 33 | { :coercer => coercer, :key_type => key_type, :value_type => value_type } 34 | } 35 | 36 | let(:key_primitive) { String } 37 | let(:value_primitive) { Integer } 38 | 39 | let(:input) { Hash[1 => '1', 2 => '2'] } 40 | 41 | it 'uses coercer to coerce key and value' do 42 | mock(coercer).call(input) { input } 43 | 44 | mock(key_type).finalize { key_type } 45 | mock(key_type).coerce(1) { '1' } 46 | mock(key_type).coerce(2) { '2' } 47 | 48 | mock(value_type).finalize { value_type } 49 | mock(value_type).coerce('1') { 1 } 50 | mock(value_type).coerce('2') { 2 } 51 | 52 | expect(subject).to eq(Hash['1' => 1, '2' => 2]) 53 | 54 | expect(key_type).to have_received.coerce(1) 55 | expect(key_type).to have_received.coerce(2) 56 | 57 | expect(value_type).to have_received.coerce('1') 58 | expect(value_type).to have_received.coerce('2') 59 | end 60 | end 61 | 62 | context 'when key/value types are EVs' do 63 | let(:key_primitive) { OpenStruct } 64 | let(:value_primitive) { Struct.new(:id) } 65 | 66 | let(:input) { Hash[{:name => 'Test'} => [1]] } 67 | let(:output) { Hash[key_primitive.new(:name => 'Test') => value_primitive.new(1)] } 68 | 69 | it 'coerces keys and values' do 70 | # FIXME: expect(subject).to eq(output) crashes in rspec 71 | expect(subject.keys.first).to eq(output.keys.first) 72 | expect(subject.values.first).to eq(output.values.first) 73 | expect(subject.size).to be(1) 74 | end 75 | end 76 | 77 | context 'when key type is an array and value type is another hash' do 78 | let(:key_primitive) { Array[String] } 79 | let(:value_primitive) { Hash[String => Integer] } 80 | 81 | let(:key_attribute) { Virtus::Attribute.build(key_primitive) } 82 | let(:value_attribute) { Virtus::Attribute.build(value_primitive) } 83 | 84 | let(:input) { Hash[[1, 2], {:one => '1', :two => '2'}] } 85 | let(:output) { Hash[key_attribute.coerce(input.keys.first) => value_attribute.coerce(input.values.first)] } 86 | 87 | it 'coerces keys and values' do 88 | expect(subject).to eq(output) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/lazy_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#lazy?' do 4 | subject { object.lazy? } 5 | 6 | let(:object) { described_class.build(String, options) } 7 | let(:options) { Hash[:lazy => lazy] } 8 | 9 | context 'when :lazy is set to true' do 10 | let(:lazy) { true } 11 | 12 | it { is_expected.to be(true) } 13 | end 14 | 15 | context 'when :lazy is set to false' do 16 | let(:lazy) { false } 17 | 18 | it { is_expected.to be(false) } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/rename_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#rename' do 4 | subject { object.rename(:bar) } 5 | 6 | let(:object) { described_class.build(String, :name => :foo, :strict => true) } 7 | let(:other) { described_class.build(String, :name => :bar, :strict => true) } 8 | 9 | describe '#name' do 10 | subject { super().name } 11 | it { is_expected.to be(:bar) } 12 | end 13 | 14 | it { is_expected.not_to be(object) } 15 | it { is_expected.to be_strict } 16 | end 17 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/required_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#required?' do 4 | subject { object.required? } 5 | 6 | let(:object) { described_class.build(String, :required => required) } 7 | 8 | context 'when required option is true' do 9 | let(:required) { true } 10 | 11 | it { is_expected.to be(true) } 12 | end 13 | 14 | context 'when required option is false' do 15 | let(:required) { false } 16 | 17 | it { is_expected.to be(false) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/set_default_value_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#set_default_value' do 4 | let(:object) { described_class.build(String, options.merge(:name => name, :default => default)) } 5 | 6 | let(:model) { Class.new { def name; 'model'; end; attr_reader :test } } 7 | let(:name) { :test } 8 | let(:instance) { model.new } 9 | let(:options) { {} } 10 | 11 | before { object.set_default_value(instance) } 12 | 13 | context 'with a nil' do 14 | subject { instance } 15 | 16 | let(:default) { nil } 17 | 18 | describe '#test' do 19 | subject { super().test } 20 | it { is_expected.to be(nil) } 21 | end 22 | 23 | describe '#instance_variables' do 24 | subject { super().instance_variables } 25 | it { is_expected.to include(:'@test') } 26 | end 27 | end 28 | 29 | context 'with a non-clonable object' do 30 | subject { instance } 31 | 32 | let(:object) { described_class.build('Boolean', options.merge(:name => name, :default => default)) } 33 | let(:default) { true } 34 | 35 | describe '#test' do 36 | subject { super().test } 37 | it { is_expected.to be(true) } 38 | end 39 | 40 | describe '#instance_variables' do 41 | subject { super().instance_variables } 42 | it { is_expected.to include(:'@test') } 43 | end 44 | end 45 | 46 | context 'with a clonable' do 47 | subject { instance } 48 | 49 | let(:default) { [] } 50 | 51 | describe '#test' do 52 | subject { super().test } 53 | it { is_expected.to eq(default) } 54 | end 55 | 56 | describe '#test' do 57 | subject { super().test } 58 | it { is_expected.not_to be(default) } 59 | end 60 | end 61 | 62 | context 'with a callable' do 63 | subject { instance } 64 | 65 | let(:default) { lambda { |model, attribute| "#{model.name}-#{attribute.name}" } } 66 | 67 | describe '#test' do 68 | subject { super().test } 69 | it { is_expected.to eq('model-test') } 70 | end 71 | end 72 | 73 | context 'with a symbol' do 74 | subject { instance } 75 | 76 | context 'when it is a method name' do 77 | let(:default) { :set_test } 78 | 79 | context 'when method is public' do 80 | let(:model) { Class.new { attr_reader :test; def set_test; @test = 'hello world'; end } } 81 | 82 | describe '#test' do 83 | subject { super().test } 84 | it { is_expected.to eq('hello world') } 85 | end 86 | end 87 | 88 | context 'when method is private' do 89 | let(:model) { Class.new { attr_reader :test; private; def set_test; @test = 'hello world'; end } } 90 | 91 | describe '#test' do 92 | subject { super().test } 93 | it { is_expected.to eq('hello world') } 94 | end 95 | end 96 | end 97 | 98 | context 'when it is not a method name' do 99 | let(:default) { :hello_world } 100 | 101 | describe '#test' do 102 | subject { super().test } 103 | it { is_expected.to eq('hello_world') } 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#set' do 4 | subject { object.set(instance, value) } 5 | 6 | let(:object) { described_class.build(String, options.merge(:name => name)) } 7 | 8 | let(:model) { Class.new { attr_reader :test } } 9 | let(:name) { :test } 10 | let(:instance) { model.new } 11 | let(:value) { 'Jane Doe' } 12 | let(:options) { {} } 13 | 14 | it { is_expected.to be(value) } 15 | 16 | context 'without coercion' do 17 | specify do 18 | expect { subject }.to change { instance.test }.to(value) 19 | end 20 | end 21 | 22 | context 'with coercion' do 23 | let(:value) { :'Jane Doe' } 24 | 25 | specify do 26 | expect { subject }.to change { instance.test }.to('Jane Doe') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute/value_coerced_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::Attribute, '#value_coerced?' do 4 | subject { object.value_coerced?(input) } 5 | 6 | let(:object) { described_class.build(String) } 7 | 8 | context 'when input is coerced' do 9 | let(:input) { '1' } 10 | 11 | it { is_expected.to be(true) } 12 | end 13 | 14 | context 'when input is not coerced' do 15 | let(:input) { 1 } 16 | 17 | it { is_expected.to be(false) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/append_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#<<' do 4 | subject { object << attribute } 5 | 6 | let(:attributes) { [] } 7 | let(:parent) { described_class.new } 8 | let(:object) { described_class.new(parent, attributes) } 9 | let(:name) { :name } 10 | 11 | context 'with a new attribute' do 12 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 13 | 14 | it { is_expected.to equal(object) } 15 | 16 | it 'adds an attribute' do 17 | expect { subject }.to change { object.to_a }. 18 | from(attributes). 19 | to([ attribute ]) 20 | end 21 | 22 | it 'indexes the new attribute under its #name property' do 23 | expect { subject }.to change { object[name] }. 24 | from(nil). 25 | to(attribute) 26 | end 27 | 28 | it 'indexes the new attribute under the string version of its #name property' do 29 | expect { subject }.to change { object[name.to_s] }. 30 | from(nil). 31 | to(attribute) 32 | end 33 | end 34 | 35 | context 'with a duplicate attribute' do 36 | let(:attributes) { [Virtus::Attribute.build(String, :name => name)] } 37 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 38 | 39 | it { is_expected.to equal(object) } 40 | 41 | it "replaces the original attribute object" do 42 | expect { subject }.to change { object.to_a.map(&:__id__) }. 43 | from(attributes.map(&:__id__)). 44 | to([attribute.__id__]) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/define_reader_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#define_reader_method' do 4 | subject(:attribute_set) { described_class.new } 5 | 6 | let(:attribute) { Virtus::Attribute.build(String, :name => method_name) } 7 | let(:method_name) { :foo_bar } 8 | 9 | before do 10 | attribute_set.define_reader_method(attribute, method_name, visibility) 11 | end 12 | 13 | context "with public visibility" do 14 | let(:visibility) { :public } 15 | 16 | it "defines public writer" do 17 | expect(attribute_set.public_instance_methods).to include(method_name) 18 | end 19 | end 20 | 21 | context "with private visibility" do 22 | let(:visibility) { :private } 23 | 24 | it "defines public writer" do 25 | expect(attribute_set.private_instance_methods).to include(method_name) 26 | end 27 | end 28 | 29 | context "with protected visibility" do 30 | let(:visibility) { :protected } 31 | 32 | it "defines protected writer" do 33 | expect(attribute_set.protected_instance_methods).to include(method_name) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/define_writer_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#define_writer_method' do 4 | subject(:attribute_set) { described_class.new } 5 | 6 | let(:attribute) { Virtus::Attribute.build(String, :name => method_name) } 7 | let(:method_name) { :foo_bar } 8 | 9 | before do 10 | attribute_set.define_writer_method(attribute, method_name, visibility) 11 | end 12 | 13 | context "with public visibility" do 14 | let(:visibility) { :public } 15 | 16 | it "defines public writer" do 17 | expect(attribute_set.public_instance_methods).to include(method_name) 18 | end 19 | end 20 | 21 | context "with private visibility" do 22 | let(:visibility) { :private } 23 | 24 | it "defines private writer" do 25 | expect(attribute_set.private_instance_methods).to include(method_name) 26 | end 27 | end 28 | 29 | context "with protected visibility" do 30 | let(:visibility) { :protected } 31 | 32 | it "defines protected writer" do 33 | expect(attribute_set.protected_instance_methods).to include(method_name) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/each_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#each' do 4 | subject(:attribute_set) { described_class.new(parent, attributes) } 5 | 6 | let(:name) { :name } 7 | let(:attribute) { Virtus::Attribute.build(String, :name => :name) } 8 | let(:attributes) { [ attribute ] } 9 | let(:parent) { described_class.new } 10 | let(:yields) { Set[] } 11 | 12 | context 'with no block' do 13 | it 'returns an enumerator when block is not provided' do 14 | expect(attribute_set.each).to be_kind_of(Enumerator) 15 | end 16 | 17 | it 'yields the expected attributes' do 18 | result = [] 19 | attribute_set.each { |attribute| result << attribute } 20 | expect(result).to eql(attributes) 21 | end 22 | end 23 | 24 | context 'with a block' do 25 | subject { attribute_set.each { |attribute| yields << attribute } } 26 | 27 | context 'when the parent has no attributes' do 28 | it { is_expected.to equal(attribute_set) } 29 | 30 | it 'yields the expected attributes' do 31 | expect { subject }.to change { yields.dup }. 32 | from(Set[]). 33 | to(attributes.to_set) 34 | end 35 | end 36 | 37 | context 'when the parent has attributes that are not duplicates' do 38 | let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } 39 | let(:parent) { described_class.new([ parent_attribute ]) } 40 | 41 | it { is_expected.to equal(attribute_set) } 42 | 43 | it 'yields the expected attributes' do 44 | result = [] 45 | 46 | attribute_set.each { |attribute| result << attribute } 47 | 48 | expect(result).to eql([parent_attribute, attribute]) 49 | end 50 | end 51 | 52 | context 'when the parent has attributes that are duplicates' do 53 | let(:parent_attribute) { Virtus::Attribute.build(String, :name => name) } 54 | let(:parent) { described_class.new([ parent_attribute ]) } 55 | 56 | it { is_expected.to equal(attribute_set) } 57 | 58 | it 'yields the expected attributes' do 59 | expect { subject }.to change { yields.dup }. 60 | from(Set[]). 61 | to(Set[ attribute ]) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/element_reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#[]' do 4 | subject { object[name] } 5 | 6 | let(:name) { :name } 7 | let(:attribute) { Virtus::Attribute.build(String, :name => :name) } 8 | let(:attributes) { [ attribute ] } 9 | let(:parent) { described_class.new } 10 | let(:object) { described_class.new(parent, attributes) } 11 | 12 | it { is_expected.to equal(attribute) } 13 | 14 | it 'allows indexed access to attributes by the string representation of their name' do 15 | expect(object[name.to_s]).to equal(attribute) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/element_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#[]=' do 4 | subject { object[name] = attribute } 5 | 6 | let(:attributes) { [] } 7 | let(:parent) { described_class.new } 8 | let(:object) { described_class.new(parent, attributes) } 9 | let(:name) { :name } 10 | 11 | context 'with a new attribute' do 12 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 13 | 14 | it { is_expected.to equal(attribute) } 15 | 16 | it 'adds an attribute' do 17 | expect { subject }.to change { object.to_a }.from(attributes).to([ attribute ]) 18 | end 19 | 20 | it 'allows #[] to access the attribute with a symbol' do 21 | expect { subject }.to change { object['name'] }.from(nil).to(attribute) 22 | end 23 | 24 | it 'allows #[] to access the attribute with a string' do 25 | expect { subject }.to change { object[:name] }.from(nil).to(attribute) 26 | end 27 | 28 | it 'allows #reset to track overridden attributes' do 29 | expect { subject }.to change { object.reset.to_a }.from(attributes).to([ attribute ]) 30 | end 31 | end 32 | 33 | context 'with a duplicate attribute' do 34 | let(:original) { Virtus::Attribute.build(String, :name => name) } 35 | let(:attributes) { [ original ] } 36 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 37 | 38 | it { is_expected.to equal(attribute) } 39 | 40 | it "replaces the original attribute object" do 41 | expect { subject }.to change { object.to_a.map(&:__id__) }. 42 | from(attributes.map(&:__id__)). 43 | to([attribute.__id__]) 44 | end 45 | 46 | it 'allows #[] to access the attribute with a string' do 47 | expect { subject }.to change { object['name'].__id__ }. 48 | from(original.__id__). 49 | to(attribute.__id__) 50 | end 51 | 52 | it 'allows #[] to access the attribute with a symbol' do 53 | expect { subject }.to change { object[:name].__id__ }. 54 | from(original.__id__). 55 | to(attribute.__id__) 56 | end 57 | 58 | it 'allows #reset to track overridden attributes' do 59 | expect { subject }.to change { object.reset.to_a.map(&:__id__) }. 60 | from(attributes.map(&:__id__)). 61 | to([attribute.__id__]) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/merge_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#merge' do 4 | subject { object.merge(other) } 5 | 6 | let(:parent) { described_class.new } 7 | let(:object) { described_class.new(parent, attributes) } 8 | let(:name) { :name } 9 | let(:other) { [attribute] } 10 | 11 | context 'with a new attribute' do 12 | let(:attributes) { [] } 13 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 14 | 15 | it { is_expected.to equal(object) } 16 | 17 | it 'adds an attribute' do 18 | expect { subject }.to change { object.to_a }.from(attributes).to([attribute]) 19 | end 20 | end 21 | 22 | context 'with a duplicate attribute' do 23 | let(:attributes) { [Virtus::Attribute.build(String, :name => name)] } 24 | let(:attribute) { Virtus::Attribute.build(String, :name => name) } 25 | 26 | it { is_expected.to equal(object) } 27 | 28 | it "replaces the original attribute object" do 29 | expect { subject }.to change { object.to_a.map(&:__id__) }. 30 | from(attributes.map(&:__id__)). 31 | to([attribute.__id__]) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_set/reset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::AttributeSet, '#reset' do 4 | subject { object.reset } 5 | 6 | let(:name) { :name } 7 | let(:attribute) { Virtus::Attribute.build(String, :name => :name) } 8 | let(:attributes) { [ attribute ] } 9 | let(:object) { described_class.new(parent, attributes) } 10 | 11 | context 'when the parent has no attributes' do 12 | let(:parent) { described_class.new } 13 | 14 | it { is_expected.to equal(object) } 15 | 16 | describe '#to_set' do 17 | subject { super().to_set } 18 | it { is_expected.to eq(Set[ attribute ]) } 19 | end 20 | end 21 | 22 | context 'when the parent has attributes that are not duplicates' do 23 | let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } 24 | let(:parent) { described_class.new([ parent_attribute ]) } 25 | 26 | it { is_expected.to equal(object) } 27 | 28 | describe '#to_set' do 29 | subject { super().to_set } 30 | it { is_expected.to eq(Set[ attribute, parent_attribute ]) } 31 | end 32 | end 33 | 34 | context 'when the parent has attributes that are duplicates' do 35 | let(:parent_attribute) { Virtus::Attribute.build(String, :name => name) } 36 | let(:parent) { described_class.new([ parent_attribute ]) } 37 | 38 | it { is_expected.to equal(object) } 39 | 40 | describe '#to_set' do 41 | subject { super().to_set } 42 | it { is_expected.to eq(Set[ attribute ]) } 43 | end 44 | end 45 | 46 | context 'when the parent has changed' do 47 | let(:parent_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } 48 | let(:parent) { described_class.new([ parent_attribute ]) } 49 | let(:new_attribute) { Virtus::Attribute.build(String, :name => :parent_name) } 50 | 51 | it { is_expected.to equal(object) } 52 | 53 | it 'includes changes from the parent' do 54 | expect(object.to_set).to eql(Set[attribute, parent_attribute]) 55 | 56 | parent << new_attribute 57 | 58 | expect(subject.to_set).to eql(Set[attribute, new_attribute]) 59 | end 60 | end 61 | 62 | context 'when the parent is nil' do 63 | let(:parent) { nil } 64 | 65 | it { is_expected.to equal(object) } 66 | 67 | it 'includes changes from the parent' do 68 | expect { subject }.to_not change { object.to_set } 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/virtus/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#attribute' do 4 | let(:name) { :test } 5 | let(:options) { {} } 6 | 7 | shared_examples_for 'a class with boolean attribute' do 8 | subject { Test } 9 | 10 | let(:object) { subject.new } 11 | 12 | it 'defines reader and writer' do 13 | object.test = true 14 | 15 | expect(object).to be_test 16 | end 17 | 18 | it 'defines predicate method' do 19 | object.test = false 20 | 21 | expect(object).to_not be_test 22 | end 23 | end 24 | 25 | shared_examples_for 'an object with string attribute' do 26 | it { is_expected.to respond_to(:test) } 27 | it { is_expected.to respond_to(:test=) } 28 | 29 | it 'can write and read the attribute' do 30 | subject.test = :foo 31 | expect(subject.test).to eql('foo') 32 | end 33 | end 34 | 35 | it 'returns self' do 36 | klass = Class.new { include Virtus } 37 | expect(klass.attribute(:test, String)).to be(klass) 38 | end 39 | 40 | it 'raises error when :name is a reserved name on a class' do 41 | klass = Class.new { include Virtus } 42 | expect { klass.attribute(:attributes, Set) }.to raise_error( 43 | ArgumentError, ':attributes is not allowed as an attribute name' 44 | ) 45 | end 46 | 47 | it 'raises error when :name is a reserved name on an instance' do 48 | object = Class.new.new.extend(Virtus) 49 | expect { object.attribute(:attributes, Set) }.to raise_error( 50 | ArgumentError, ':attributes is not allowed as an attribute name' 51 | ) 52 | end 53 | 54 | it 'allows :attributes as an attribute name when mass-assignment is not included' do 55 | klass = Class.new { include Virtus::Model::Core } 56 | klass.attribute(:attributes, Set) 57 | expect(klass.attribute_set[:attributes]).to be_instance_of(Virtus::Attribute::Collection) 58 | end 59 | 60 | it 'allows specifying attribute without type' do 61 | klass = Class.new { include Virtus::Model::Core } 62 | klass.attribute(:name) 63 | expect(klass.attribute_set[:name]).to be_instance_of(Virtus::Attribute) 64 | end 65 | 66 | context 'with a class' do 67 | context 'when type is Boolean' do 68 | before :all do 69 | class Test 70 | include Virtus 71 | 72 | attribute :test, Boolean 73 | end 74 | end 75 | 76 | after :all do 77 | Object.send(:remove_const, :Test) 78 | end 79 | 80 | it_behaves_like 'a class with boolean attribute' 81 | end 82 | 83 | context 'when type is "Boolean"' do 84 | before :all do 85 | class Test 86 | include Virtus 87 | 88 | attribute :test, 'Boolean' 89 | end 90 | end 91 | 92 | after :all do 93 | Object.send(:remove_const, :Test) 94 | end 95 | 96 | it_behaves_like 'a class with boolean attribute' 97 | end 98 | 99 | context 'when type is Axiom::Types::Boolean' do 100 | before :all do 101 | class Test 102 | include Virtus 103 | 104 | attribute :test, Axiom::Types::Boolean 105 | end 106 | end 107 | 108 | after :all do 109 | Object.send(:remove_const, :Test) 110 | end 111 | 112 | it_behaves_like 'a class with boolean attribute' do 113 | before do 114 | pending 'this will be fixed once Attribute::Boolean subclass is gone' 115 | end 116 | end 117 | end 118 | 119 | context 'when type is :Boolean' do 120 | before :all do 121 | class Test 122 | include Virtus 123 | 124 | attribute :test, 'Boolean' 125 | end 126 | end 127 | 128 | after :all do 129 | Object.send(:remove_const, :Test) 130 | end 131 | 132 | it_behaves_like 'a class with boolean attribute' 133 | 134 | context 'with a subclass' do 135 | it_behaves_like 'a class with boolean attribute' do 136 | subject { Class.new(Test) } 137 | 138 | it 'gets attributes from the parent class' do 139 | Test.attribute :other, Integer 140 | expect(subject.attribute_set[:other]).to eql(Test.attribute_set[:other]) 141 | end 142 | end 143 | end 144 | end 145 | 146 | context 'when type is Decimal' do 147 | before :all do 148 | class Test 149 | include Virtus 150 | 151 | attribute :test, Decimal 152 | end 153 | end 154 | 155 | after :all do 156 | Object.send(:remove_const, :Test) 157 | end 158 | 159 | it 'maps type to the corresponding axiom type' do 160 | expect(Test.attribute_set[:test].type).to be(Axiom::Types::Decimal) 161 | end 162 | end 163 | end 164 | 165 | context 'with a module' do 166 | let(:mod) { 167 | Module.new { 168 | include Virtus 169 | 170 | attribute :test, String 171 | } 172 | } 173 | 174 | let(:model) { Class.new } 175 | 176 | context 'included in the class' do 177 | before do 178 | model.send(:include, mod) 179 | end 180 | 181 | it 'adds attributes from the module to a class that includes it' do 182 | expect(model.attribute_set[:test]).to be_instance_of(Virtus::Attribute) 183 | end 184 | 185 | it_behaves_like 'an object with string attribute' do 186 | subject { model.new } 187 | end 188 | end 189 | 190 | context 'included in the class' do 191 | it_behaves_like 'an object with string attribute' do 192 | subject { model.new.extend(mod) } 193 | end 194 | end 195 | end 196 | 197 | context 'with an instance' do 198 | subject { model.new } 199 | 200 | let(:model) { Class.new } 201 | 202 | before do 203 | subject.extend(Virtus) 204 | subject.attribute(:test, String) 205 | end 206 | 207 | it_behaves_like 'an object with string attribute' 208 | end 209 | 210 | context 'using custom module' do 211 | subject { model.new } 212 | 213 | let(:model) { 214 | Class.new { 215 | include Virtus.model { |config| config.coerce = false } 216 | 217 | attribute :test, String 218 | } 219 | } 220 | 221 | it { is_expected.to respond_to(:test) } 222 | it { is_expected.to respond_to(:test=) } 223 | 224 | it 'writes and reads attributes' do 225 | subject.test = :foo 226 | expect(subject.test).to be(:foo) 227 | end 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /spec/unit/virtus/attributes_reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#attributes' do 4 | 5 | shared_examples_for 'attribute hash' do 6 | it 'includes all attributes' do 7 | subject.attributes = { :test => 'Hello World', :test_priv => 'Yo' } 8 | 9 | expect(subject.attributes).to eql(:test => 'Hello World') 10 | end 11 | end 12 | 13 | context 'with a class' do 14 | let(:model) { 15 | Class.new { 16 | include Virtus 17 | 18 | attribute :test, String 19 | attribute :test_priv, String, :reader => :private 20 | } 21 | } 22 | 23 | it_behaves_like 'attribute hash' do 24 | subject { model.new } 25 | end 26 | end 27 | 28 | context 'with an instance' do 29 | subject { model.new } 30 | 31 | let(:model) { Class.new } 32 | 33 | before do 34 | subject.extend(Virtus) 35 | subject.attribute :test, String 36 | subject.attribute :test_priv, String, :reader => :private 37 | end 38 | 39 | it_behaves_like 'attribute hash' 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/virtus/attributes_writer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#attributes=' do 4 | 5 | shared_examples_for 'mass-assignment' do 6 | it 'allows writing known attributes' do 7 | subject.attributes = { :test => 'Hello World' } 8 | 9 | expect(subject.test).to eql('Hello World') 10 | end 11 | 12 | it 'skips writing unknown attributes' do 13 | subject.attributes = { :test => 'Hello World', :nothere => 'boom!' } 14 | 15 | expect(subject.test).to eql('Hello World') 16 | end 17 | end 18 | 19 | context 'with a class' do 20 | let(:model) { 21 | Class.new { 22 | include Virtus 23 | 24 | attribute :test, String 25 | } 26 | } 27 | 28 | it_behaves_like 'mass-assignment' do 29 | subject { model.new } 30 | end 31 | 32 | it 'raises when attributes is not hash-like object' do 33 | expect { model.new('not a hash, really') }.to raise_error( 34 | NoMethodError, 'Expected "not a hash, really" to respond to #to_hash' 35 | ) 36 | end 37 | end 38 | 39 | context 'with an instance' do 40 | subject { model.new } 41 | 42 | let(:model) { Class.new } 43 | 44 | before do 45 | subject.extend(Virtus) 46 | subject.attribute :test, String 47 | end 48 | 49 | it_behaves_like 'mass-assignment' 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/unit/virtus/class_methods/finalize_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '.finalize' do 4 | before do 5 | module Examples 6 | class Person 7 | include Virtus.model(:finalize => false) 8 | 9 | attribute :name, String 10 | attribute :articles, Array['Examples::Article'] 11 | attribute :address, :'Examples::Address' 12 | end 13 | 14 | class Article 15 | include Virtus.model(:finalize => false) 16 | 17 | attribute :posts, Hash['Examples::Person' => 'Examples::Post'] 18 | attribute :person, :'Examples::Person' 19 | end 20 | 21 | class Post 22 | include Virtus.model 23 | 24 | attribute :title, String 25 | end 26 | 27 | class Address 28 | include Virtus.model 29 | 30 | attribute :street, String 31 | end 32 | end 33 | 34 | expect(Examples::Post.attribute_set[:title]).to be_finalized 35 | expect(Examples::Address.attribute_set[:street]).to be_finalized 36 | 37 | expect(Virtus::Builder.pending).not_to include(Examples::Post) 38 | expect(Virtus::Builder.pending).not_to include(Examples::Address) 39 | 40 | Virtus.finalize 41 | end 42 | 43 | it "sets attributes that don't require finalization" do 44 | expect(Examples::Person.attribute_set[:name]).to be_instance_of(Virtus::Attribute) 45 | expect(Examples::Person.attribute_set[:name].primitive).to be(String) 46 | end 47 | 48 | it 'it finalizes member type for a collection attribute' do 49 | expect(Examples::Person.attribute_set[:address].primitive).to be(Examples::Address) 50 | end 51 | 52 | it 'it finalizes key type for a hash attribute' do 53 | expect(Examples::Article.attribute_set[:posts].key_type.primitive).to be(Examples::Person) 54 | end 55 | 56 | it 'it finalizes value type for a hash attribute' do 57 | expect(Examples::Article.attribute_set[:posts].value_type.primitive).to be(Examples::Post) 58 | end 59 | 60 | it 'it finalizes type for an EV attribute' do 61 | expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person) 62 | end 63 | 64 | it 'automatically resolves constant when it is already available' do 65 | expect(Examples::Article.attribute_set[:person].type.primitive).to be(Examples::Person) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/unit/virtus/class_methods/new_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '.new' do 4 | let(:model) { 5 | Class.new { 6 | include Virtus 7 | 8 | attribute :id, Integer 9 | attribute :name, String, :default => 'John Doe' 10 | attribute :email, String, :default => 'john@doe.com', :lazy => true, :writer => :private 11 | } 12 | } 13 | 14 | context 'without attribute hash' do 15 | subject { model.new } 16 | 17 | it 'sets default values for non-lazy attributes' do 18 | expect(subject.instance_variable_get('@name')).to eql('John Doe') 19 | end 20 | 21 | it 'skips setting default values for lazy attributes' do 22 | expect(subject.instance_variable_get('@email')).to be(nil) 23 | end 24 | end 25 | 26 | context 'with attribute hash' do 27 | subject { model.new(:id => 1, :name => 'Jane Doe') } 28 | 29 | it 'sets attributes with public writers' do 30 | expect(subject.id).to be(1) 31 | expect(subject.name).to eql('Jane Doe') 32 | end 33 | 34 | it 'skips setting attributes with private writers' do 35 | expect(subject.instance_variable_get('@email')).to be(nil) 36 | end 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/unit/virtus/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '.config' do 4 | it 'provides global configuration' do 5 | Virtus.config { |config| config.coerce = false } 6 | 7 | expect(Virtus.coerce).to be(false) 8 | 9 | Virtus.config { |config| config.coerce = true } 10 | 11 | expect(Virtus.coerce).to be(true) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/unit/virtus/element_reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#[]' do 4 | subject { object[:test] } 5 | 6 | let(:model) { 7 | Class.new { 8 | include Virtus 9 | 10 | attribute :test, String 11 | } 12 | } 13 | 14 | let(:object) { model.new } 15 | 16 | before do 17 | object.test = 'foo' 18 | end 19 | 20 | it { is_expected.to eq('foo') } 21 | end 22 | -------------------------------------------------------------------------------- /spec/unit/virtus/element_writer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#[]=' do 4 | subject { object[:test] = 'foo' } 5 | 6 | let(:model) { 7 | Class.new { 8 | include Virtus 9 | 10 | attribute :test, String 11 | } 12 | } 13 | 14 | let(:object) { model.new } 15 | 16 | specify do 17 | expect { subject }.to change { object.test }.from(nil).to('foo') 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/virtus/freeze_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#freeze' do 4 | subject { object.freeze } 5 | 6 | let(:model) { 7 | Class.new { 8 | include Virtus 9 | 10 | attribute :name, String, :default => 'foo', :lazy => true 11 | attribute :age, Integer, :default => 30 12 | attribute :rand, Float, :default => Proc.new { rand } 13 | } 14 | } 15 | 16 | let(:object) { model.new } 17 | 18 | it { is_expected.to be_frozen } 19 | 20 | describe '#name' do 21 | subject { super().name } 22 | it { is_expected.to eql('foo') } 23 | end 24 | 25 | describe '#age' do 26 | subject { super().age } 27 | it { is_expected.to be(30) } 28 | end 29 | 30 | it "does not change dynamic default values" do 31 | original_value = object.rand 32 | object.freeze 33 | expect(object.rand).to eq original_value 34 | end 35 | 36 | it "does not change default attributes that have been explicitly set" do 37 | object.rand = 3.14 38 | object.freeze 39 | expect(object.rand).to eq 3.14 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/virtus/model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '.model' do 4 | shared_examples_for 'a model with constructor' do 5 | it 'accepts attribute hash' do 6 | instance = subject.new(:name => 'Jane') 7 | expect(instance.name).to eql('Jane') 8 | end 9 | end 10 | 11 | shared_examples_for 'a model with mass-assignment' do 12 | let(:attributes) do 13 | { :name => 'Jane', :something => nil } 14 | end 15 | 16 | before do 17 | instance.attributes = attributes 18 | end 19 | 20 | it 'accepts attribute hash' do 21 | expect(instance.attributes).to eql(attributes) 22 | end 23 | end 24 | 25 | shared_examples_for 'a model with strict mode turned off' do 26 | it 'has attributes with strict set to false' do 27 | expect(subject.send(:attribute_set)[:name]).to_not be_strict 28 | end 29 | end 30 | 31 | context 'with default configuration' do 32 | let(:mod) { Virtus.model } 33 | 34 | context 'with a class' do 35 | let(:model) { Class.new } 36 | 37 | subject { model } 38 | 39 | before do 40 | subject.send(:include, mod) 41 | subject.attribute :name, String, :default => 'Jane' 42 | subject.attribute :something 43 | end 44 | 45 | it_behaves_like 'a model with constructor' 46 | 47 | it_behaves_like 'a model with strict mode turned off' 48 | 49 | it_behaves_like 'a model with mass-assignment' do 50 | let(:instance) { subject.new } 51 | end 52 | 53 | it 'defaults to Object for attribute type' do 54 | expect(model.attribute_set[:something].type).to be(Axiom::Types::Object) 55 | end 56 | 57 | context 'with a sub-class' do 58 | subject { Class.new(model) } 59 | 60 | before do 61 | subject.attribute :age, Integer 62 | end 63 | 64 | it_behaves_like 'a model with constructor' 65 | 66 | it_behaves_like 'a model with strict mode turned off' 67 | 68 | it_behaves_like 'a model with mass-assignment' do 69 | let(:instance) { subject.new } 70 | 71 | let(:attributes) { 72 | { :name => 'Jane', :something => nil, :age => 23 } 73 | } 74 | end 75 | 76 | it 'has its own attributes' do 77 | expect(subject.attribute_set[:age]).to be_kind_of(Virtus::Attribute) 78 | end 79 | end 80 | end 81 | 82 | context 'with an instance' do 83 | subject { Class.new.new } 84 | 85 | before do 86 | subject.extend(mod) 87 | subject.attribute :name, String 88 | subject.attribute :something 89 | end 90 | 91 | it_behaves_like 'a model with strict mode turned off' 92 | 93 | it_behaves_like 'a model with mass-assignment' do 94 | let(:instance) { subject } 95 | end 96 | end 97 | end 98 | 99 | context 'when constructor is disabled' do 100 | subject { Class.new.send(:include, mod) } 101 | 102 | let(:mod) { Virtus.model(:constructor => false) } 103 | 104 | it 'does not accept attribute hash in the constructor' do 105 | expect { subject.new({}) }.to raise_error(ArgumentError) 106 | end 107 | end 108 | 109 | context 'when strict mode is enabled' do 110 | let(:mod) { Virtus.model(:strict => true) } 111 | let(:model) { Class.new } 112 | 113 | context 'with a class' do 114 | subject { model.new } 115 | 116 | before do 117 | model.send(:include, mod) 118 | model.attribute :name, String 119 | end 120 | 121 | it 'has attributes with strict set to true' do 122 | expect(model.attribute_set[:name]).to be_strict 123 | end 124 | end 125 | 126 | context 'with an instance' do 127 | subject { model.new } 128 | 129 | before do 130 | subject.extend(mod) 131 | subject.attribute :name, String 132 | end 133 | 134 | it 'has attributes with strict set to true' do 135 | expect(subject.send(:attribute_set)[:name]).to be_strict 136 | end 137 | end 138 | end 139 | 140 | context 'when mass-assignment is disabled' do 141 | let(:mod) { Virtus.model(:mass_assignment => false) } 142 | let(:model) { Class.new } 143 | 144 | context 'with a class' do 145 | subject { model.new } 146 | 147 | before do 148 | model.send(:include, mod) 149 | end 150 | 151 | it { is_expected.not_to respond_to(:attributes) } 152 | it { is_expected.not_to respond_to(:attributes=) } 153 | end 154 | 155 | context 'with an instance' do 156 | subject { model.new } 157 | 158 | before do 159 | subject.extend(mod) 160 | end 161 | 162 | it { is_expected.not_to respond_to(:attributes) } 163 | it { is_expected.not_to respond_to(:attributes=) } 164 | end 165 | end 166 | 167 | context 'when :required is set' do 168 | let(:mod) { Virtus.model(:required => false) } 169 | let(:model) { Class.new } 170 | 171 | context 'with a class' do 172 | subject { model.new } 173 | 174 | before do 175 | model.send(:include, mod) 176 | model.attribute :name, String 177 | end 178 | 179 | it 'has attributes with :required option inherited from module' do 180 | expect(model.attribute_set[:name]).to_not be_required 181 | end 182 | end 183 | 184 | context 'with an instance' do 185 | subject { model.new } 186 | 187 | before do 188 | subject.extend(mod) 189 | subject.attribute :name, String 190 | end 191 | 192 | it 'has attributes with strict set to true' do 193 | expect(subject.send(:attribute_set)[:name]).not_to be_required 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /spec/unit/virtus/module_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '.module' do 4 | shared_examples_for 'a valid virtus object' do 5 | it 'reads and writes attribute' do 6 | instance.name = 'John' 7 | expect(instance.name).to eql('John') 8 | end 9 | end 10 | 11 | shared_examples_for 'an object extended with virtus module' do 12 | context 'with default configuration' do 13 | subject { Virtus.module } 14 | 15 | it_behaves_like 'a valid virtus object' do 16 | let(:instance) { model.new } 17 | end 18 | 19 | it 'sets defaults' do 20 | expect(instance.name).to eql('Jane') 21 | end 22 | end 23 | 24 | context 'with constructor turned off' do 25 | subject { Virtus.module(:constructor => false) } 26 | 27 | it_behaves_like 'a valid virtus object' do 28 | let(:instance) { model.new } 29 | end 30 | 31 | it 'skips including constructor' do 32 | expect { model.new({}) }.to raise_error(ArgumentError) 33 | end 34 | end 35 | 36 | context 'with mass assignment is turned off' do 37 | subject { Virtus.module(:mass_assignment => false) } 38 | 39 | it_behaves_like 'a valid virtus object' 40 | 41 | it 'skips including mass assignment' do 42 | expect(instance).not_to respond_to(:attributes) 43 | expect(instance).not_to respond_to(:attributes=) 44 | end 45 | end 46 | 47 | context 'with coercion turned off' do 48 | subject { Virtus.module(:coerce => false) } 49 | 50 | it_behaves_like 'a valid virtus object' 51 | 52 | it 'builds non-coercible attributes' do 53 | expect(object.send(:attribute_set)[:name]).not_to be_coercible 54 | end 55 | end 56 | end 57 | 58 | let(:mod) { Module.new } 59 | let(:model) { Class.new } 60 | let(:instance) { model.new } 61 | 62 | before do 63 | mod.send(:include, subject) 64 | mod.attribute :name, String, :default => 'Jane' 65 | mod.attribute :something 66 | end 67 | 68 | context 'with a class' do 69 | let(:object) { model } 70 | 71 | before do 72 | model.send(:include, mod) 73 | end 74 | 75 | it 'provides attributes for the model' do 76 | expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) 77 | end 78 | 79 | it 'defaults to Object for attribute type' do 80 | expect(model.attribute_set[:something].type).to be(Axiom::Types::Object) 81 | end 82 | 83 | it_behaves_like 'an object extended with virtus module' 84 | end 85 | 86 | context 'with a model instance' do 87 | let(:object) { instance } 88 | 89 | before do 90 | instance.extend(mod) 91 | end 92 | 93 | it 'provides attributes for the instance' do 94 | expect(instance.send(:attribute_set)[:name]).to be_kind_of(Virtus::Attribute) 95 | end 96 | 97 | it_behaves_like 'an object extended with virtus module' 98 | end 99 | 100 | context 'with another module' do 101 | let(:other) { Module.new } 102 | 103 | let(:object) { instance } 104 | 105 | before do 106 | other.send(:include, mod) 107 | model.send(:include, other) 108 | end 109 | 110 | it_behaves_like 'an object extended with virtus module' 111 | 112 | it 'provides attributes for the model' do 113 | expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) 114 | end 115 | end 116 | 117 | context 'as a peer to another module within a class' do 118 | subject { Virtus.module } 119 | let(:other) { Module.new } 120 | 121 | before do 122 | other.send(:include, Virtus.module) 123 | other.attribute :last_name, String, :default => 'Doe' 124 | other.attribute :something_else 125 | model.send(:include, mod) 126 | model.send(:include, other) 127 | end 128 | 129 | it 'provides attributes for the model from both modules' do 130 | expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) 131 | expect(model.attribute_set[:something]).to be_kind_of(Virtus::Attribute) 132 | expect(model.attribute_set[:last_name]).to be_kind_of(Virtus::Attribute) 133 | expect(model.attribute_set[:something_else]).to be_kind_of(Virtus::Attribute) 134 | end 135 | 136 | it 'includes the attributes from both modules' do 137 | expect(model.new.attributes.keys).to eq( 138 | [:name, :something, :last_name, :something_else] 139 | ) 140 | end 141 | end 142 | 143 | context 'with multiple other modules mixed into it' do 144 | subject { Virtus.module } 145 | let(:other) { Module.new } 146 | let(:yet_another) { Module.new } 147 | 148 | before do 149 | other.send(:include, Virtus.module) 150 | other.attribute :last_name, String, :default => 'Doe' 151 | other.attribute :something_else 152 | yet_another.send(:include, Virtus.module) 153 | yet_another.send(:include, mod) 154 | yet_another.send(:include, other) 155 | yet_another.attribute :middle_name, String, :default => 'Foobar' 156 | model.send(:include, yet_another) 157 | end 158 | 159 | it 'provides attributes for the model from all modules' do 160 | expect(model.attribute_set[:name]).to be_kind_of(Virtus::Attribute) 161 | expect(model.attribute_set[:something]).to be_kind_of(Virtus::Attribute) 162 | expect(model.attribute_set[:last_name]).to be_kind_of(Virtus::Attribute) 163 | expect(model.attribute_set[:something_else]).to be_kind_of(Virtus::Attribute) 164 | expect(model.attribute_set[:middle_name]).to be_kind_of(Virtus::Attribute) 165 | end 166 | 167 | it 'includes the attributes from all modules' do 168 | expect(model.new.attributes.keys).to eq( 169 | [:name, :something, :last_name, :something_else, :middle_name] 170 | ) 171 | end 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /spec/unit/virtus/set_default_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus, '#set_default_attributes!' do 4 | subject { object.set_default_attributes! } 5 | 6 | let(:model) { 7 | Class.new { 8 | include Virtus 9 | 10 | attribute :name, String, :default => 'foo', :lazy => true 11 | attribute :age, Integer, :default => 30 12 | } 13 | } 14 | 15 | let(:object) { model.new } 16 | 17 | before do 18 | object.set_default_attributes! 19 | end 20 | 21 | it { is_expected.to be(object) } 22 | 23 | describe '#name' do 24 | subject { super().name } 25 | it { is_expected.to eql('foo') } 26 | end 27 | 28 | describe '#age' do 29 | subject { super().age } 30 | it { is_expected.to be(30) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/virtus/value_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Virtus::ValueObject do 4 | shared_examples_for 'a valid value object' do 5 | subject { model.new(attributes) } 6 | 7 | let(:attributes) { Hash[:id => 1, :name => 'Jane Doe'] } 8 | 9 | describe '#id' do 10 | subject { super().id } 11 | it { is_expected.to be(1) } 12 | end 13 | 14 | describe '#name' do 15 | subject { super().name } 16 | it { is_expected.to eql('Jane Doe') } 17 | end 18 | 19 | it 'sets private writers' do 20 | expect(subject.class.attribute_set[:id]).to_not be_public_writer 21 | expect(subject.class.attribute_set[:name]).to_not be_public_writer 22 | end 23 | 24 | it 'disallows cloning' do 25 | expect(subject.clone).to be(subject) 26 | end 27 | 28 | it 'defines #eql?' do 29 | expect(subject).to eql(subject.class.new(attributes)) 30 | end 31 | 32 | it 'defines #==' do 33 | expect(subject == subject.class.new(attributes)).to be(true) 34 | end 35 | 36 | it 'defines #hash' do 37 | expect(subject.hash).to eql(subject.class.new(attributes).hash) 38 | end 39 | 40 | it 'defines #inspect' do 41 | expect(subject.inspect).to eql( 42 | %(#) 43 | ) 44 | end 45 | 46 | it 'allows to construct new values using #with' do 47 | new_instance = subject.with(:name => "John Doe") 48 | expect(new_instance.id).to eql(subject.id) 49 | expect(new_instance.name).to eql("John Doe") 50 | end 51 | end 52 | 53 | shared_examples_for 'a valid value object with mass-assignment turned on' do 54 | subject { model.new } 55 | 56 | it 'disallows mass-assignment' do 57 | expect(subject.private_methods).to include(:attributes=) 58 | end 59 | end 60 | 61 | context 'using new values {} block' do 62 | let(:model) { 63 | model = Virtus.value_object(:coerce => false, :mass_assignment => mass_assignment) 64 | 65 | Class.new { 66 | include model 67 | 68 | def self.name 69 | 'Model' 70 | end 71 | 72 | values do 73 | attribute :id, Integer 74 | attribute :name, String 75 | end 76 | } 77 | } 78 | 79 | context 'without mass-assignment' do 80 | let(:mass_assignment) { false } 81 | 82 | it_behaves_like 'a valid value object' 83 | end 84 | 85 | context 'with mass-assignment' do 86 | let(:mass_assignment) { true } 87 | 88 | it_behaves_like 'a valid value object' 89 | it_behaves_like 'a valid value object with mass-assignment turned on' 90 | 91 | context 'with a model subclass' do 92 | let(:subclass) { 93 | Class.new(model) { 94 | values do 95 | attribute :email, String 96 | end 97 | } 98 | } 99 | 100 | it_behaves_like 'a valid value object' do 101 | subject { subclass.new(attributes) } 102 | 103 | let(:attributes) { Hash[:id => 1, :name => 'Jane Doe', :email => 'jane@doe.com'] } 104 | 105 | describe '#email' do 106 | subject { super().email } 107 | it { is_expected.to eql('jane@doe.com') } 108 | end 109 | 110 | it 'sets private writers for additional values' do 111 | expect(subclass.attribute_set[:email]).to_not be_public_writer 112 | end 113 | 114 | it 'defines valid #== for a subclass' do 115 | expect(subject == subject.class.new(attributes.merge(:id => 2))).to be(false) 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | context 'using deprecated inclusion' do 123 | let(:model) { 124 | Class.new { 125 | include Virtus::ValueObject 126 | 127 | def self.name 128 | 'Model' 129 | end 130 | 131 | attribute :id, Integer 132 | attribute :name, String 133 | } 134 | } 135 | 136 | it_behaves_like 'a valid value object' 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /virtus.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path('../lib/virtus/version', __FILE__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "virtus" 7 | gem.version = Virtus::VERSION.dup 8 | gem.authors = [ "Piotr Solnica" ] 9 | gem.email = [ "piotr.solnica@gmail.com" ] 10 | gem.description = "Attributes on Steroids for Plain Old Ruby Objects" 11 | gem.summary = gem.description 12 | gem.homepage = "https://github.com/solnic/virtus" 13 | gem.license = 'MIT' 14 | 15 | gem.require_paths = [ "lib" ] 16 | gem.files = `git ls-files`.split("\n") 17 | gem.test_files = `git ls-files -- {spec}/*`.split("\n") 18 | gem.extra_rdoc_files = %w[LICENSE README.md TODO.md] 19 | 20 | gem.add_dependency('descendants_tracker', '~> 0.0', '>= 0.0.3') 21 | gem.add_dependency('coercible', '~> 1.0') 22 | gem.add_dependency('axiom-types', '~> 0.1') 23 | 24 | gem.add_development_dependency 'rake' 25 | gem.required_ruby_version = '>= 2.0' 26 | end 27 | --------------------------------------------------------------------------------