diff --git a/.gitignore b/.gitignore index 0702df0..d9dda18 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ spec/reports test/tmp test/version_tmp tmp +tags .idea/.name .idea/.rakeTasks .idea/dictionaries/bsundarraj.xml @@ -24,4 +25,4 @@ tmp .idea/plain_old_model.iml .idea/scopes/scope_settings.xml .idea/vcs.xml -.idea/workspace.xml \ No newline at end of file +.idea/workspace.xml diff --git a/Gemfile b/Gemfile index ff9747e..e4cf322 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,14 @@ source 'https://rubygems.org' # Specify your gem's dependencies in plain_old_model.gemspec gemspec + +group :test do + gem 'simplecov', :require => false +end + +group :development do + # gem 'pry', :require => false + gem 'guard', require: false + gem 'guard-rspec', require: false +end + diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..30f07ff --- /dev/null +++ b/Guardfile @@ -0,0 +1,25 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +guard :rspec do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^spec/(.+)/.+_spec\.rb$}) + watch(%r{^lib/plain_old_model/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } + + # # Rails example + # watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + # watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + # watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } + # watch(%r{^spec/support/(.+)\.rb$}) { "spec" } + # watch('config/routes.rb') { "spec/routing" } + # watch('app/controllers/application_controller.rb') { "spec/controllers" } + # + # # Capybara features specs + # watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } + # + # # Turnip features and steps + # watch(%r{^spec/acceptance/(.+)\.feature$}) + # watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } +end + diff --git a/README.md b/README.md index f406b34..b199f56 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,84 @@ -# PlainOldModel +# PlainOldModel # -TODO: Write a gem description +Implements nested attribute mass-assignment and has_many and has_one associations, and also pulls in: -## Installation +* ActiveModel::Naming +* ActiveModel::Translation +* ActiveModel::Validations +* ActiveModel::Conversion -Add this line to your application's Gemfile: - - gem 'plain_old_model' -And then execute: +### Installation ### - $ bundle +Add this line to your application's Gemfile: -Or install it yourself as: +```ruby +gem install plain_old_model +``` - $ gem install plain_old_model +or run -## Usage -Example -======= +```ruby +gem 'plain_old_model' +``` - class Person < PlainOldModel::Base - attr_accessor :name, :age, :book - attr_reader :account_number - attr_writer :address +### Usage ### - validates_presence_of :book - end +```ruby +class Person < PlainOldModel::Base + attr_accessor :name + validates_presence_of :book +end - params = {"name" =>"testmeparams", "age" => "25", "book" =>["wewrwrwr", "werwrwrr"]} +params = {"name" =>"Anna", :book => {:author =>"Tolstoy", :category => "fiction"}} - params1 = {:name =>"testmeparams", :age => "25", :book => {:author =>"my name", :category => "fiction"}} p = Person.new(params) - -p.book # ["wewrwrwr", "werwrwrr"] +p.book p.valid? #true +``` + + +### Mass Assignment ### + +```ruby +p.assign_attributes({name: "Anna", book: {author: "Tolstoy", category: "fiction"}}) +``` + +or - OR - -p = Person.new() +```ruby +p.attributes = {:name =>"Anna", :book => {:author =>"Tolstoy", :category => "fiction"}} +``` -p.assign_attributes(params11) -===================================================================== - p1 = Person.new(params1) +### Associations ### - p1.book # {:author =>"my name", :category => "fiction"} +* has_one +* has_many - p.attributes #[:name, :age, :book, :account_number, :address] +```ruby +class Person < PlainOldModel::Base + has_many :phones +end +``` +or, with optional factory_method and class_name: +```ruby +class Person < PlainOldModel::Base + has_many :phones, factory_method: :create, class_name: :telephone +end -TODO: +class Telephone < PlainOldModel::Base + attr_accessor :number, :extension -* Association(s) -* mass assignments -* + def self.create(attributes) + attributes[:extension] = '000' + new(attributes) + end +end +``` -## Contributing -1. Fork it -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create new Pull Request diff --git a/examples/book.rb b/examples/book.rb index 4b9dd81..68ef8cd 100644 --- a/examples/book.rb +++ b/examples/book.rb @@ -1,6 +1,7 @@ require_relative '../lib/plain_old_model/base' + class Book < PlainOldModel::Base attr_accessor :author, :category -end \ No newline at end of file +end diff --git a/lib/plain_old_model/attribute_assignment.rb b/lib/plain_old_model/attribute_assignment.rb index 6837c74..20a60dc 100644 --- a/lib/plain_old_model/attribute_assignment.rb +++ b/lib/plain_old_model/attribute_assignment.rb @@ -1,8 +1,29 @@ -require 'active_support/all' +require 'active_support/core_ext/hash/deep_merge' +require 'active_support/core_ext/hash/indifferent_access' module PlainOldModel module AttributeAssignment + + def assign_attributes(new_attributes, options = {}) + return unless new_attributes + @new_attributes = new_attributes.dup + assign_attributes_from_associations + assign_simple_attributes + end + + alias attributes= assign_attributes + + def self.included(klass) + klass.extend ClassMethods + end + + def associations + self.class.associations + end + + module ClassMethods + def has_one(attr_name, options={}) associations << HasOneAssociation.new(attr_name, options) attr_accessor attr_name @@ -19,29 +40,6 @@ def associations end - def self.included(klass) - klass.extend ClassMethods - end - - def assign_attributes(new_attributes, options = {}) - return unless new_attributes - attributes = new_attributes.dup - - associations.each do |association| - attr_name = association.attr_name - if attributes.include?(attr_name) - merged_hash = merge_association_instance_variables_with_attributes(association, attr_name, attributes) - value = association.create_value_from_attributes(merged_hash) - set_attribute(attr_name, value) - attributes = attributes.delete_if { |key, value| key.to_s == attr_name.to_s } - end - end - assign_simple_attributes(attributes, options) - end - - def associations - self.class.associations - end class Association attr_reader :attr_name @@ -72,10 +70,11 @@ def klass_from_attr_name end end - class HasOneAssociation < Association + class HasOneAssociation < Association end + class HasManyAssociation < Association def create_value_from_attributes(items) items.map{|item| super(item)} @@ -86,65 +85,84 @@ def klass_from_attr_name end end + private - def sanitize_attributes(attributes) - sanitized_attributes = {} - attributes.each do |k, v| - if respond_to?("#{k}=") || respond_to?("#{k}") - sanitized_attributes[k]=v + def assign_attributes_from_associations + associations.each do |association| + assign_attributes_from_association(association) end end - sanitized_attributes - end - - def assign_simple_attributes(attributes, options) - attributes = sanitize_attributes(attributes).stringify_keys - attributes.each do |k, v| - set_attribute(k, v) + def assign_attributes_from_association(association) + attr_name = association.attr_name + if @new_attributes.include?(attr_name) + merged_hash = merged_association_hash(association, attr_name) + value = association.create_value_from_attributes(merged_hash) + set_attribute(attr_name, value) + @new_attributes.delete_if { |key, value| key.to_s == attr_name.to_s } + end end - end - def set_attribute(attr_name, value) - if respond_to?("#{attr_name}=") - send("#{attr_name}=", value) - else - instance_variable_set("@#{attr_name}".to_sym, value) + # deep merge of instance variables from one association + # with the rest of the 'new_attributes' + def merged_association_hash(association, attr_name) + association_instance = send(attr_name) + + if association.class == HasOneAssociation + return instance_variables_hash(association_instance).deep_merge(@new_attributes[attr_name]) + elsif association.class == HasManyAssociation + if association_instance.nil? + return @new_attributes[attr_name] + else + association_instance_array = [] + association_instance.each_with_index do |instance, i| + association_instance_array << instance_variables_hash(instance).deep_merge(@new_attributes[attr_name][i]) + end + return association_instance_array + end + end end - end - def merge_association_instance_variables_with_attributes(association, attr_name, attributes) - association_instance = send(attr_name) - if association.class == HasOneAssociation - instance_hash = create_association_hash(association_instance,HashWithIndifferentAccess.new) - merged_result = instance_hash.deep_merge(attributes[attr_name]) - elsif association.class == HasManyAssociation - association_instance_array = [] - if association_instance.nil? - merged_result = attributes[attr_name] - else - for i in 0..association_instance.length-1 - instance_hash = create_association_hash(association_instance[i],HashWithIndifferentAccess.new) - association_instance_array << instance_hash.deep_merge(attributes[attr_name][i]) + def instance_variables_hash(association_instance) + hash = HashWithIndifferentAccess.new + + unless association_instance.nil? + association_instance.instance_variables.each do |var| + if association_instance.instance_variable_get(var).instance_variables.length > 0 + hash[var.to_s.delete("@")] = instance_variables_hash(association_instance.instance_variable_get(var)) + else + hash[var.to_s.delete("@")] = association_instance.instance_variable_get(var) + end end - merged_result = association_instance_array end + hash end - merged_result - end - def create_association_hash(association_instance,association_instance_hash) - unless association_instance.nil? - association_instance.instance_variables.each do |var| - if association_instance.instance_variable_get(var).instance_variables.length > 0 - association_instance_hash[var.to_s.delete("@")] = create_association_hash(association_instance.instance_variable_get(var),HashWithIndifferentAccess.new) - else - association_instance_hash[var.to_s.delete("@")] = association_instance.instance_variable_get(var) + def assign_simple_attributes + sanitized_attributes.stringify_keys.each do |k, v| + set_attribute(k, v) + end + end + + # attributes for which we have an attr_reader and/or an attr_writer + def sanitized_attributes + sanitized = {} + @new_attributes.each do |k, v| + if respond_to?("#{k}=") || respond_to?("#{k}") + sanitized[k] = v end end + sanitized end - association_instance_hash - end + + def set_attribute(attr_name, value) + if respond_to?("#{attr_name}=") + send("#{attr_name}=", value) + else + instance_variable_set("@#{attr_name}".to_sym, value) + end + end + end end diff --git a/lib/plain_old_model/base.rb b/lib/plain_old_model/base.rb index 7ddcf6c..b1cfb14 100644 --- a/lib/plain_old_model/base.rb +++ b/lib/plain_old_model/base.rb @@ -21,5 +21,4 @@ def persisted? end end - -end \ No newline at end of file +end diff --git a/spec/lib/assignment_spec.rb b/spec/lib/assignment_spec.rb new file mode 100644 index 0000000..51914bd --- /dev/null +++ b/spec/lib/assignment_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe PlainOldModel::Base do + + describe "creating a new model" do + + it "accepts the params and instantiate with variables initialized" do + @person= Person.new( + {:fname => "Tom", + :address => { + :city => 'Oso', + :state => 'WA' + } + }) + @person.fname.should == "Tom" + @person.address.should == {:city => 'Oso', :state => 'WA'} + end + + it "eliminates the params that are not available in the class" do + @person= Person.new({:fname => "Tom", :foo => "bar"}) + @person.valid?.should == true + @person.instance_variable_get(:@foo).should == nil + end + + it "supports activemodel validations" do + @person= Person.new({:lname => "Jobim"}) + @person.valid?.should == false + @person.errors.should_not == nil + end + + end + + describe "assigning attributes to an existing model" do + before(:each) do + @address = Address.new + @address.assign_attributes({:state => "WA"}) + @address.attributes = {:city => "Oakland"} + end + + it "assigns value to the attr_reader attributes/ read only attribute" do + @address.state.should == "WA" + end + + it "supports the assignment operator syntax" do + @address.city.should == "Oakland" + end + + end +end + diff --git a/spec/lib/associations_spec.rb b/spec/lib/associations_spec.rb new file mode 100644 index 0000000..9d29f74 --- /dev/null +++ b/spec/lib/associations_spec.rb @@ -0,0 +1,202 @@ +require 'spec_helper' + +describe PlainOldModel::Base do + + describe "associations" do + + it "returns empty array for unassociated class" do + continent = Continent.new + continent.associations.should == [] + end + + it "provides all the associations when the class has associations" do + address = Address.new + address.associations.length.should == 1 + address.associations.first.attr_name.should == :country + end + + describe "a has_one association" do + before(:each) do + @address = Address.new( + { + :city => "Mumbai", + :state => "Maharashtra", + :country => { + :code => "In", :name => "India", + :continent => {:name => "Asia", :desc => {:actual_desc => "big"}}}}) + end + + it "assigns attributes to the associated class" do + @address.country.class.should == Country + end + + it "creates the nested class instance and assign_attributes to the associated nested class" do + @address.country.continent.class.should == Continent + end + + it "creates a new instance and assign_attributes to the associated class" do + @address.country.continent.name.should == "Asia" + end + + it "assigns values to the read only attributes" do + address = Address.new( + { + :city => "Mumbai", + :state => "Maharashtra", + :country => {:code => "In", :name => "India"}, + :readable => 'yes' + }) + address.readable.should == "yes" + end + + it "overrides assigned attributes" do + address = Address.new( + { + :city => "Mumbai", + :country => {:code => "In", :name => "India"}, + :readable => 'should be assigned'}) + address.assign_attributes( + { + :city => "Oso", + :country => {:code => "USA", :name => "United States"} + }) + address.city.should == "Oso" + address.country.code.should == "USA" + address.country.name.should == "United States" + address.readable.should == "should be assigned" + end + + it "should not overwrite unassigned nested attributes" do + address = Address.new( + { + :city => "Mumbai", + :state => "Maharashtra", + :country => {:code => "In", :name => "India"}}) + address.assign_attributes({:city => "Bangalore"}) + address.city.should == "Bangalore" + address.country.code.should == "In" + address.country.name.should == "India" + end + + it "should not overwrite unassigned nested attributes' values" do + address = Address.new( + { + :city => "Mumbai", + :country => {:code => "In", :name => "India", :continent => {:name => "asia" }}, + :readable => 'yes'}) + address.assign_attributes( + {"city" => "Oso", + :country => {:name => "United States", :continent => {:desc => {:text => "large landmass"}}}}) + address.city.should == "Oso" + address.country.code.should == "In" + address.country.name.should == "United States" + address.country.continent.name.should == "asia" + address.readable.should == "yes" + end + + it "creates assigned nested attributes" do + address = Address.new({:state => "AK", :readable => 'should be assigned'}) + address.assign_attributes({"city" => "Oso", :country => {:code => "In", :name => "India"} }) + address.city.should == "Oso" + address.country.code.should == "In" + address.country.name.should == "India" + address.readable.should == "should be assigned" + end + + it "should assigned nested attributes with mixed string and symbol hash keys" do + address = Address.new( + { + :city => "Seattle", + :country => {:code => "US", :name => "USA"}, + :readable => 'should be assigned' + }) + address.attributes = {:city => "Mumbai", :country => {"code" => "In", "name" => "India"} } + address.city.should == "Mumbai" + address.country.code.should == "In" + address.country.name.should == "India" + address.readable.should == "should be assigned" + end + + end + + describe "construction of a model with a has_many association" do + + it "should create a new instance and assign attributes for each value in array" do + person = Person.new({ addresses: [{ city: "Oso", state: "WA"}, + { city: "Oakland", state: "CA"}]}) + person.addresses.first.class.should == Address + person.addresses.first.city.should == "Oso" + person.addresses.first.state.should == "WA" + person.addresses[1].class.should == Address + person.addresses[1].city.should == "Oakland" + person.addresses[1].state.should == "CA" + end + end + + describe "assignment to a model with a has_many association" do + + + it "should not alter the params passed in" do + passed_params = { addresses: [ + { :city => "Oso", :state => "WA"}, + { :city => "Oakland", :state => "CA"} + ]} + person = Person.new(passed_params) + person.addresses.length.should == 2 + person.addresses.first.class.should == Address + person.addresses.first.city.should == "Oso" + passed_params.should == { addresses: [ + { :city => "Oso", :state => "WA"}, + { :city => "Oakland", :state => "CA"} + ]} + end + + it "should create each class via factory_method if one is specified" do + person = Person.new({ phones: [{ number: '5841'}, {number: '9139'}]}) + person.phones.length.should == 2 + person.phones[0].number.should == '5841' + person.phones[0].extension.should == 'set_via_factory' + person.phones[1].extension.should == 'set_via_factory' + end + end + + describe "mass-assignment to a model with a has_many association" do + before(:all) do + @person = Person.new({addresses: [ + { + :state => "Gujarat", + :country => { + :continent => {:name => "Asia"}} + }, + { + :state => "AK", + }]}) + + @person.attributes = {addresses: [ + { + :city => "Surat", + :country => { + :name => "India", + :continent => {:desc => "large"} + } + }, + { + state: "CA" + }]} + end + + it "should overwrite assigned nested attributes' values" do + @person.addresses.last.state.should == "CA" + @person.addresses.first.country.name.should == "India" + end + + it "should not affect unassigned nested attributes' values" do + @person.addresses.first.state.should == "Gujarat" + @person.addresses.first.country.continent.name.should == "Asia" + end + + end + end +end + + diff --git a/spec/lib/base_spec.rb b/spec/lib/base_spec.rb index 98ab31e..54eef9c 100644 --- a/spec/lib/base_spec.rb +++ b/spec/lib/base_spec.rb @@ -2,184 +2,12 @@ describe PlainOldModel::Base do - describe " assign_attribute and new" do + describe "assign_attribute and new" do + it "should accept the params and new up a class with variables initialized" do @person= Person.new({:fname => "first value", :lname => "second value", :address => {:fname => 'fname', :lname => 'lname'}}) - @person.fname.should == "first value" - @person.lname.should == "second value" - @person.address.should == {:fname => 'fname', :lname => 'lname'} - end - it "should assign value to the attr_reader attributes/ read only attribute" do - @address = Address.new - @address.fname = "first value" - @address.assign_attributes({:lname => "second value", :read_test => 'This should be assigned'}) - @address.fname.should == "first value" - @address.read_test.should == "This should be assigned" - end - it "should assign value to the attr_writer attributes" do - @address = Address.new - @address.assign_attributes({:fname => "first value", :lname => "second value", :read_test => 'This should not be assigned',:write_test => "this shd be available"}) - @address.instance_variable_get(:@write_test).should == "this shd be available" - end - it "should assign_attributes to the class" do - @person = Person.new - @person.assign_attributes({:fname => "first value", :lname => "second value"}) - @person.fname.should == "first value" - @person.lname.should == "second value" - end - it "should eliminate the params that are not available in the class" do - @person= Person.new({:fname => "first value", :lname => "second value"}) - @person.valid?.should == true - end - it "should allow the class to use activemodel validations and errors" do - @person= Person.new({:lname => "second value"}) - @person.valid?.should == false - @person.errors.should_not == nil - end - end - - describe "associations" do - it "should return empty array for unassociated class" do - @person = Continent.new - @person.associations.should == [] - end - it "should provide all the associations when the class has associations" do - @address = Address.new - @address.associations.length.should == 1 - @address.associations.first.attr_name.should == :country - end - describe "has_one" do - it "should create a new instance and assign_attributes to the associated class" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India"}, :read_test => 'This should not be assigned',:write_test => "this shd be available"}) - @address.country.class.should == Country - end - it "should create the nested class instance and assign_attributes to the associated nested class" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India", :continent => {:name => "asia"}}, :read_test => 'This should not be assigned',:write_test => "this shd be available"}) - @address.country.continent.class.should == Continent - end - it "should create a new instance and assign_attributes to the associated class" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India", :continent => {:name => "asia", :desc => {:this => "is a test", :actual_desc => "is another test"}}}, :read_test => 'This should not be assigned',:write_test => "this shd be available"}) - @address.country.continent.class.should == Continent - @address.country.continent.name.should == "asia" - @continent = @address.country.continent - @continent.name.should == "asia" - end - it "should assign values to the read only attributes" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India"}, :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.read_test.should == "This should be assigned" - end - it "should override assigned attributes" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India"}, :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.assign_attributes({:fname => "replaced first value", :lname => "replaced second value", :country => {:code => "USA", :name => "United States"}}) - @address.fname.should == "replaced first value" - @address.country.code.should == "USA" - @address.country.name.should == "United States" - @address.read_test.should == "This should be assigned" - end - it "should not override unassigned nested attributes" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India"}, :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.assign_attributes({:fname => "replaced first value", :lname => "replaced second value"}) - @address.fname.should == "replaced first value" - @address.country.code.should == "In" - @address.country.name.should == "India" - @address.read_test.should == "This should be assigned" - end - it "should not override unassigned nested attributes' values" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "In", :name => "India", :continent => {:name => "asia", :desc => {:this => "is a test", :actual_desc => "is another test"}}}, :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.assign_attributes({"fname" => "replaced first value", :lname => "replaced second value", :country => {:name => "United States", :continent => {:desc => {:this => "is a replacement", :actual_desc => "is another replacement"}}}}) - @address.fname.should == "replaced first value" - @address.country.code.should == "In" - @address.country.name.should == "United States" - @address.country.continent.name.should == "asia" - @address.read_test.should == "This should be assigned" - end - it "should create assigned nested attributes" do - @address = Address.new({:lname => "second value", :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.assign_attributes({"fname" => "first value", :lname => "replaced second value", :country => {:code => "In", :name => "India"} }) - @address.fname.should == "first value" - @address.country.code.should == "In" - @address.country.name.should == "India" - @address.read_test.should == "This should be assigned" - end - it "should assigned nested attributes with mixed string and symbol hash keys" do - @address = Address.new({:fname => "first value", :lname => "second value", :country => {:code => "", :name => ""}, :read_test => 'This should be assigned',:write_test => "this shd be available"}) - @address.assign_attributes({:fname => "replaced first value", :lname => "replaced second value", :country => {"code" => "In", "name" => "India"} }) - @address.fname.should == "replaced first value" - @address.country.code.should == "In" - @address.country.name.should == "India" - @address.read_test.should == "This should be assigned" - end + @person.persisted?.should == false end - describe "has_many" do - it "should create a new instance and assign attributes for each value in array" do - @person = Person.new({ addresses: [{ fname: "first name 1", lname: "last name 1"}, { fname: "first name 2", lname: "last name 2"}]}) - @person.addresses.length.should == 2 - @person.addresses.first.class.should == Address - @person.addresses.first.fname.should == "first name 1" - @person.addresses.first.lname.should == "last name 1" - @person.addresses[1].class.should == Address - @person.addresses[1].fname.should == "first name 2" - @person.addresses[1].lname.should == "last name 2" - end - it "should not alter the params passed in" do - passed_params = { addresses: [{ fname: "first name 1", lname: "last name 1"}, { fname: "first name 2", lname: "last name 2"}]} - @person = Person.new(passed_params) - @person.addresses.length.should == 2 - @person.addresses.first.class.should == Address - @person.addresses.first.fname.should == "first name 1" - passed_params.should == { addresses: [{ fname: "first name 1", lname: "last name 1"}, { fname: "first name 2", lname: "last name 2"}]} - end - it "should create each class via factory_method if one is specified" do - @person = Person.new({ phones: [{ number: '423-5841'}, {number: '383-9139'}]}) - @person.phones.length.should == 2 - @person.phones[0].number.should == '423-5841' - @person.phones[0].extension.should == 'set_via_factory' - @person.phones[1].extension.should == 'set_via_factory' - end - it "should not override unassigned nested attributes' values" do - @person = Person.new({ addresses: [{ fname: "first name 1", lname: "last name 1", :country => {:code => "In", :name => "India", :continent => {:name => "asia", :desc => {:this => "is a test", :actual_desc => "is another test"}}}}, { fname: "first name 2", lname: "last name 2"}]}) - @person.assign_attributes({ addresses: [{ fname: "first name 1", :country => {:name => "United States", :continent => {:desc => {:this => "is a replacement", :actual_desc => "is another replacement"}}}}, { fname: "first name 2", lname: "last name 2"}]}) - @person.addresses.first.lname.should == "last name 1" - @person.addresses.last.lname.should == "last name 2" - @person.addresses.first.country.name.should == "United States" - @person.addresses.first.country.continent.name.should == "asia" - end - end end end - -class Person < PlainOldModel::Base - attr_accessor :fname, :lname, :address - has_many :addresses - has_many :phones, factory_method: :create - validates_presence_of :fname -end - -class Phone < PlainOldModel::Base - attr_accessor :number, :extension - - def self.create(attributes) - attributes[:extension] = 'set_via_factory' - new(attributes) - end -end - -class Address < PlainOldModel::Base - attr_accessor :fname, :lname - attr_reader :read_test - attr_writer :write_test - - has_one :country -end - -class Country < PlainOldModel::Base - attr_accessor :code, :name - - has_one :continent -end - -class Continent < PlainOldModel::Base - attr_accessor :name, :desc -end - diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e1bbb26..ae1ce10 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,6 +4,10 @@ # loaded once. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'simplecov' +SimpleCov.start + require 'plain_old_model' require 'plain_old_model/base' require 'plain_old_model/version' @@ -20,3 +24,39 @@ # --seed 1234 config.order = 'random' end + + +class Telephone < PlainOldModel::Base + attr_accessor :number, :extension + + def self.create(attributes) + attributes[:extension] = 'set_via_factory' + new(attributes) + end +end + +class Person < PlainOldModel::Base + attr_accessor :fname, :lname, :address + has_many :addresses + has_many :phones, factory_method: :create, class_name: :telephone + validates_presence_of :fname +end + +class Address < PlainOldModel::Base + attr_accessor :city, :state + attr_reader :readable + attr_writer :writable + + has_one :country +end + +class Country < PlainOldModel::Base + attr_accessor :code, :name + + has_one :continent +end + +class Continent < PlainOldModel::Base + attr_accessor :name, :desc +end +