├── .coveralls.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Changelog.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── active_model_attributes.gemspec ├── bin ├── console └── setup ├── lib ├── active_model_attributes.rb └── active_model_attributes │ └── version.rb └── spec ├── active_model_attributes_spec.rb └── spec_helper.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.13.6 6 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # ActiveModelAttributes Changelog 2 | 3 | ## master 4 | 5 | ## 1.6.0 6 | 7 | - [FEATURE] Add `attribute_types` method to match behaviour that Rails 6 ActiveModel::Attributes provides by @shevaun 8 | 9 | ## 1.5.0 10 | 11 | - [ENHANCEMENT] Initialize field with default value upon read by @jughead 12 | 13 | ## 1.4.0 14 | 15 | - [FEATURE] Add methods for introspection of types and attributes by @fsateler 16 | 17 | ## 1.3.0 18 | 19 | - [FEATURE] Allow untyped attributes by defaulting to the `ActiveModel::Type::Value` type by @alan 20 | 21 | ## 1.2.0 22 | 23 | Changes: 24 | - [ENHANCEMENT] Wrap definition of attributes' readers and writers inside anonymous modules to be able to call `super` by @Azdaroth 25 | 26 | ## 1.1.0 27 | 28 | Changes: 29 | - [BUGFIX] Pass `options` argument to `ActiveModel::Type.lookup` in attribute writers by @jughead 30 | 31 | ## 1.0.0 32 | 33 | Update notes: 34 | - Breaking change: the attribute's setter now calls #cast method on type instead of #deserialize. If you have defined any custom type, just rename the method from #deserialize to #cast 35 | 36 | Changes: 37 | - [ENHANCEMENT] Use #cast method from types instead of #deserialize by @Azdaroth 38 | 39 | ## 0.1.0 40 | 41 | Update notes: 42 | - None 43 | 44 | Changes: 45 | - [FEATURE] Basic implementation by @Azdaroth 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_model_attributes.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Karol Galanciak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveModelAttributes [![Build Status](https://travis-ci.org/Azdaroth/active_model_attributes.svg?branch=master)](https://travis-ci.org/Azdaroth/active_model_attributes) [![Gem Version](https://badge.fury.io/rb/active_model_attributes.svg)](https://rubygems.org/gems/active_model_attributes) [![Coverage Status](https://coveralls.io/repos/github/Azdaroth/active_model_attributes/badge.svg)](https://coveralls.io/github/Azdaroth/active_model_attributes) 2 | 3 | Rails 5.0 comes with a great addition of ActiveRecord Attributes API. However, that's only for ActiveRecord, you can't really use it in your ActiveModel models. Fortunately, with this gem it's possible. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'active_model_attributes' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install active_model_attributes 20 | 21 | ## Usage 22 | 23 | Define your ActiveModel model class, include `ActiveModel::Model` and `ActiveModelAttributes` modules and define attributes and their types using `attribute` class method: 24 | 25 | ``` rb 26 | class MyAwesomeModel 27 | include ActiveModel::Model 28 | include ActiveModelAttributes 29 | 30 | attribute :integer_field, :integer 31 | attribute :string_field, :string 32 | attribute :decimal_field, :decimal 33 | attribute :boolean_field, :boolean 34 | end 35 | ``` 36 | 37 | You can also provide a default value for each attribute (either a raw value or a lambda): 38 | 39 | ``` rb 40 | class MyAwesomeModel 41 | include ActiveModel::Model 42 | include ActiveModelAttributes 43 | 44 | attribute :string_with_default, :string, default: "default string" 45 | attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) } 46 | end 47 | ``` 48 | 49 | You can get the list of defined attributes, their types and provided options by accessing `attributes_registry` class attribute, for instance: 50 | 51 | ``` rb 52 | class MyAwesomeModel 53 | include ActiveModel::Model 54 | include ActiveModelAttributes 55 | 56 | attribute :string_with_default, :string, default: "default string" 57 | end 58 | ``` 59 | 60 | ``` 61 | MyAwesomeModel.attributes_registry 62 | => { string_with_default: [:string, { default: "default string" }] } 63 | ``` 64 | 65 | Here's a list of supported types: 66 | 67 | * big_integer 68 | * binary 69 | * boolean 70 | * date 71 | * datetime 72 | * decimal 73 | * float 74 | * immutable_string 75 | * integer 76 | * string 77 | * time 78 | 79 | You can also add your custom types. Just create a class inheriting from `ActiveModel::Type::Value` or already existing type, e.g. `ActiveModel::Type::Integer`, define `cast` method and register the new type: 80 | 81 | ``` rb 82 | class SomeCustomMoneyType < ActiveModel::Type::Integer 83 | def cast(value) 84 | return super if value.kind_of?(Numeric) 85 | return super if !value.to_s.include?('$') 86 | 87 | price_in_dollars = BigDecimal.new(value.gsub(/\$/, '')) 88 | super(price_in_dollars * 100) 89 | end 90 | end 91 | 92 | ActiveModel::Type.register(:money, SomeCustomMoneyType) 93 | ``` 94 | 95 | Now you can use this type inside you ActiveModel models: 96 | 97 | ``` 98 | class ModelForAttributesTestWithCustomType 99 | include ActiveModel::Model 100 | include ActiveModelAttributes 101 | 102 | attribute :price, :money 103 | end 104 | 105 | data = ModelForAttributesTestWithCustomType.new 106 | data.price = "$100.12" 107 | data.price 108 | => 10012 109 | ``` 110 | 111 | ## Development 112 | 113 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 114 | 115 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 116 | 117 | ## Contributing 118 | 119 | Bug reports and pull requests are welcome on GitHub at https://github.com/Azdaroth/active_model_attributes. 120 | 121 | 122 | ## License 123 | 124 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 125 | 126 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /active_model_attributes.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_model_attributes/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_model_attributes" 8 | spec.version = ActiveModelAttributes::VERSION 9 | spec.authors = ["Karol Galanciak"] 10 | spec.email = ["karol.galanciak@gmail.com"] 11 | 12 | spec.summary = %q{ActiveModel extension with support for ActiveRecord-like Attributes API} 13 | spec.description = %q{ActiveModel extension with support for ActiveRecord-like Attributes API} 14 | spec.homepage = "https://github.com/Azdaroth/active_model_attributes" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = "exe" 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 2.1.2" 25 | spec.add_development_dependency "rake", "~> 12.0" 26 | spec.add_development_dependency "rspec", "~> 3.0" 27 | spec.add_development_dependency "pry" 28 | if RUBY_VERSION >= "2.4" 29 | spec.add_development_dependency "pry-byebug" 30 | end 31 | spec.add_development_dependency "coveralls" 32 | 33 | spec.add_dependency "activemodel", ">= 5.0" 34 | spec.add_dependency "activesupport", ">= 5.0" 35 | end 36 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "active_model_attributes" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/active_model_attributes.rb: -------------------------------------------------------------------------------- 1 | require "active_model_attributes/version" 2 | require "active_support/concern" 3 | require "active_model/type" 4 | 5 | module ActiveModelAttributes 6 | extend ActiveSupport::Concern 7 | 8 | delegate :type_for_attribute, :has_attribute?, to: :class 9 | 10 | included do 11 | class_attribute :attributes_registry, :attribute_types, instance_accessor: false 12 | self.attributes_registry = {} 13 | self.attribute_types = Hash.new(ActiveModel::Type.default_value) 14 | end 15 | 16 | module ClassMethods 17 | NO_DEFAULT_PROVIDED = Object.new 18 | SERVICE_ATTRIBUTES = %i(default user_provided_default).freeze 19 | private_constant :NO_DEFAULT_PROVIDED 20 | 21 | def attribute(name, cast_type = ActiveModel::Type::Value.new, **options) 22 | self.attributes_registry = attributes_registry.merge(name => [cast_type, options]) 23 | 24 | define_attribute_reader(name, options) 25 | define_attribute_writer(name, cast_type, options) 26 | 27 | if cast_type.is_a?(Symbol) 28 | cast_type = ActiveModel::Type.lookup(cast_type, **options.except(:default)) 29 | end 30 | self.attribute_types = attribute_types.merge(name.to_s => cast_type) 31 | end 32 | 33 | def define_attribute_reader(name, options) 34 | wrapper = Module.new do 35 | provided_default = options.fetch(:default) { NO_DEFAULT_PROVIDED } 36 | define_method name do 37 | return instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}") 38 | return if provided_default == NO_DEFAULT_PROVIDED 39 | instance_variable_set("@#{name}", provided_default.respond_to?(:call) && provided_default.call || provided_default) 40 | end 41 | end 42 | include wrapper 43 | end 44 | 45 | def define_attribute_writer(name, cast_type, options) 46 | wrapper = Module.new do 47 | define_method "#{name}=" do |val| 48 | if cast_type.is_a?(Symbol) 49 | cast_type = ActiveModel::Type.lookup(cast_type, **options.except(*SERVICE_ATTRIBUTES)) 50 | end 51 | deserialized_value = cast_type.cast(val) 52 | instance_variable_set("@#{name}", deserialized_value) 53 | end 54 | end 55 | include wrapper 56 | end 57 | 58 | def type_for_attribute(attr) 59 | type_desc = attributes_registry[attr.to_sym] 60 | return ActiveModel::Type::Value.new if type_desc.nil? 61 | 62 | if type_desc[0].is_a?(Symbol) 63 | type, options = type_desc 64 | ActiveModel::Type.lookup(type, **options.except(*SERVICE_ATTRIBUTES)) 65 | else 66 | type_desc[0] 67 | end 68 | end 69 | 70 | def has_attribute?(attr) 71 | attributes_registry.key?(attr.to_sym) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/active_model_attributes/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveModelAttributes 2 | VERSION = "1.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/active_model_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "active_model" 3 | 4 | describe ActiveModelAttributes do 5 | class ModelForAttributesTest 6 | include ActiveModel::Model 7 | include ActiveModelAttributes 8 | 9 | attribute :integer_field, :integer 10 | attribute :string_field, :string 11 | attribute :decimal_field, :decimal 12 | attribute :string_with_default, :string, default: "default string" 13 | attribute :date_field, :date, default: -> { Date.new(2016, 1, 1) } 14 | attribute :boolean_field, :boolean 15 | attribute :boolean_with_type, ActiveModel::Type::Boolean.new 16 | attribute :mutable_field, ActiveModel::Type::Value.new, default: -> { {some_array: []} } 17 | end 18 | 19 | class ChildModelForAttributesTest < ModelForAttributesTest 20 | end 21 | 22 | class GrandchildModelForAttributesTest < ChildModelForAttributesTest 23 | attribute :integer_field, :string 24 | end 25 | 26 | class SomeCustomMoneyType < ActiveModel::Type::Integer 27 | def cast(value) 28 | return super if value.kind_of?(Numeric) 29 | return super if !value.to_s.include?('$') 30 | 31 | price_in_dollars = BigDecimal.new(value.gsub(/\$/, '')) 32 | super(price_in_dollars * 100) 33 | end 34 | end 35 | ActiveModel::Type.register(:money, SomeCustomMoneyType) 36 | 37 | class ModelForAttributesTestWithCustomType 38 | include ActiveModel::Model 39 | include ActiveModelAttributes 40 | 41 | attribute :price, :money 42 | end 43 | 44 | class CustomStructWithOptionableType 45 | include ActiveModel::Model 46 | include ActiveModelAttributes 47 | 48 | attribute :setting, :integer 49 | attribute :something_else, :string 50 | end 51 | 52 | class CustomStructWithOptionsType < ActiveModel::Type::Value 53 | 54 | attr_reader :setting 55 | 56 | def initialize(options={}) 57 | @setting = options.delete(:setting) 58 | super(options) 59 | end 60 | 61 | def cast(value) 62 | case value 63 | when Hash 64 | CustomStructWithOptionableType.new(value.merge(setting: @setting)) 65 | else 66 | CustomStructWithOptionableType.new(setting: @setting) 67 | end 68 | end 69 | end 70 | 71 | ActiveModel::Type.register(:custom_struct, CustomStructWithOptionsType) 72 | 73 | class ModelForAttributesTestWithCustomTypeWithOptions 74 | include ActiveModel::Model 75 | include ActiveModelAttributes 76 | 77 | attribute :custom_struct1, :custom_struct, setting: 42 78 | attribute :custom_struct2, :custom_struct, setting: 43 79 | end 80 | 81 | class ModelForAttributesTestWithOverridenReader 82 | include ActiveModel::Model 83 | include ActiveModelAttributes 84 | 85 | attribute :value, :string 86 | 87 | def value 88 | super.to_s + " overridden" 89 | end 90 | end 91 | 92 | class ModelForAttributesTestWithOverridenWriter 93 | include ActiveModel::Model 94 | include ActiveModelAttributes 95 | 96 | attribute :value, :string 97 | 98 | def value=(val) 99 | super(val.to_s.upcase) 100 | end 101 | end 102 | 103 | class ModelForAttributesTestWithDefaultType 104 | include ActiveModel::Model 105 | include ActiveModelAttributes 106 | 107 | attribute :value 108 | attribute :other_value, default: 'foo' 109 | end 110 | 111 | it "handles attributes assignment with default type and with a default value" do 112 | data = ModelForAttributesTestWithDefaultType.new(value: { foo: 'bar' }) 113 | 114 | expect(data.value).to eq(foo: 'bar') 115 | expect(data.other_value).to eq 'foo' 116 | end 117 | 118 | it "handles attributes assignment with proper type and with proper defaults" do 119 | data = ModelForAttributesTest.new( 120 | integer_field: "2.3", 121 | string_field: "Rails FTW", 122 | decimal_field: "12.3", 123 | boolean_field: "0" 124 | ) 125 | 126 | expect(data.integer_field).to eq 2 127 | expect(data.string_field).to eq "Rails FTW" 128 | expect(data.decimal_field).to eq BigDecimal.new("12.3") 129 | expect(data.string_with_default).to eq "default string" 130 | expect(data.date_field).to eq Date.new(2016, 1, 1) 131 | expect(data.boolean_field).to eq false 132 | 133 | data.integer_field = 10 134 | data.string_with_default = nil 135 | data.boolean_field = "1" 136 | 137 | expect(data.integer_field).to eq 10 138 | expect(data.string_with_default).to eq nil 139 | expect(data.boolean_field).to eq true 140 | end 141 | 142 | it "raises error when assigning nonexistent attribute" do 143 | expect { 144 | ModelForAttributesTest.new(nonexistent: "nonexistent") 145 | }.to raise_error ActiveModel::UnknownAttributeError 146 | end 147 | 148 | it "handles attributes' inheritance" do 149 | data = ChildModelForAttributesTest.new(integer_field: "4.4") 150 | 151 | expect(data.integer_field).to eq 4 152 | end 153 | 154 | it "handles overriding of attributes in children from parents" do 155 | data = GrandchildModelForAttributesTest.new(integer_field: "4.4") 156 | 157 | expect(data.integer_field).to eq "4.4" 158 | end 159 | 160 | it "has registry of attributes with passed options" do 161 | expected_attributes_keys = [ 162 | :integer_field, 163 | :string_field, 164 | :decimal_field, 165 | :string_with_default, 166 | :date_field, 167 | :boolean_field, 168 | :boolean_with_type, 169 | :mutable_field 170 | ] 171 | registry = GrandchildModelForAttributesTest.attributes_registry 172 | 173 | expect(registry.keys).to eq expected_attributes_keys 174 | expect(registry[:integer_field]).to eq [:string, {}] 175 | expect(registry[:decimal_field]).to eq [:decimal, {}] 176 | expect(registry[:string_with_default]).to eq [:string, { default: "default string" }] 177 | expect(registry[:date_field].last[:default].call).to eq Date.new(2016, 1, 1) 178 | end 179 | 180 | it "works with custom types" do 181 | data = ModelForAttributesTestWithCustomType.new 182 | 183 | expect(data.price).to eq nil 184 | 185 | data.price = "$100.12" 186 | 187 | expect(data.price).to eq 10012 188 | end 189 | 190 | it "works with custom types and given specific options" do 191 | data = ModelForAttributesTestWithCustomTypeWithOptions.new( 192 | custom_struct1: {something_else: '12'}, 193 | custom_struct2: {something_else: '23'} 194 | ) 195 | expect(data.custom_struct1.setting).to eq 42 196 | expect(data.custom_struct2.setting).to eq 43 197 | expect(data.custom_struct1.something_else).to eq '12' 198 | expect(data.custom_struct2.something_else).to eq '23' 199 | end 200 | 201 | it "is possible to use `super` inside attribute reader" do 202 | data = ModelForAttributesTestWithOverridenReader.new(value: "value") 203 | 204 | expect(data.value).to eq "value overridden" 205 | end 206 | 207 | it "is possible to use `super` inside attribute writer" do 208 | data = ModelForAttributesTestWithOverridenWriter.new(value: "value") 209 | 210 | expect(data.value).to eq "VALUE" 211 | end 212 | 213 | it "checks available attributes" do 214 | data = ModelForAttributesTest.new 215 | 216 | # works with both symbol and string 217 | expect(data.has_attribute?("integer_field")).to eq true 218 | expect(data.has_attribute?(:integer_field)).to eq true 219 | expect(data.has_attribute?("nonexisting_field")).to eq false 220 | expect(data.has_attribute?(:nonexisting_field)).to eq false 221 | end 222 | 223 | describe ".attribute_types" do 224 | it "returns a hash of attribute names with their type information" do 225 | expect(ModelForAttributesTest.attribute_types['integer_field']).to be_an_instance_of ActiveModel::Type::Integer 226 | expect(ModelForAttributesTest.attribute_types['string_field']).to be_an_instance_of ActiveModel::Type::String 227 | expect(ModelForAttributesTest.attribute_types['decimal_field']).to be_an_instance_of ActiveModel::Type::Decimal 228 | expect(ModelForAttributesTest.attribute_types['boolean_with_type']).to be_an_instance_of ActiveModel::Type::Boolean 229 | end 230 | end 231 | 232 | it "returns type information for available attributes" do 233 | data = ModelForAttributesTest.new 234 | 235 | expect(data.type_for_attribute(:integer_field)).to be_an_instance_of ActiveModel::Type::Integer 236 | expect(data.type_for_attribute(:string_field)).to be_an_instance_of ActiveModel::Type::String 237 | expect(data.type_for_attribute(:decimal_field)).to be_an_instance_of ActiveModel::Type::Decimal 238 | expect(data.type_for_attribute(:boolean_with_type)).to be_an_instance_of ActiveModel::Type::Boolean 239 | end 240 | 241 | it "returns type information with options for available attributes" do 242 | data = ModelForAttributesTestWithCustomTypeWithOptions.new 243 | 244 | expect(data.type_for_attribute(:custom_struct1).setting).to be 42 245 | expect(data.type_for_attribute(:custom_struct2).setting).to be 43 246 | end 247 | 248 | it "returns type information for nonexistent attributes" do 249 | data = ModelForAttributesTest.new 250 | 251 | expect(data.type_for_attribute(:nonexistent_field)).to be_an_instance_of ActiveModel::Type::Value 252 | end 253 | 254 | it "initilizes default values and stores them in instance variables" do 255 | data = ModelForAttributesTest.new 256 | expect(data.mutable_field).to eq(some_array: []) 257 | expect(data.mutable_field.object_id).to eq data.mutable_field.object_id 258 | object_id = data.mutable_field.object_id 259 | data.mutable_field[:some_array] << 12 260 | expect(data.mutable_field.object_id).to eq object_id 261 | expect(data.mutable_field[:some_array]).to eq [12] 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require 'coveralls' 3 | 4 | Coveralls.wear! 5 | 6 | require "active_model_attributes" 7 | require "pry" 8 | --------------------------------------------------------------------------------