# frozen_string_literal: true require 'spec_helper' module Aws module Record describe 'Attributes' do let(:klass) do Class.new do include(Aws::Record) end end describe '#initialize' do let(:model) do Class.new do include(Aws::Record) string_attr(:id, hash_key: true) string_attr(:body) end end it 'should allow attribute assignment at item creation time' do item = model.new(id: 'MyId') expect(item.id).to eq('MyId') expect(item.body).to be_nil end it 'should allow assignment of multiple attributes at item creation' do item = model.new(id: 'MyId', body: 'Hello!') expect(item.id).to eq('MyId') expect(item.body).to eq('Hello!') end end describe 'Keys' do it 'should be able to assign a hash key' do klass.string_attr(:mykey, hash_key: true) klass.string_attr(:other) expect(klass.hash_key).to eq(:mykey) end it 'should be able to assign a hash and range key' do klass.string_attr(:mykey, hash_key: true) klass.string_attr(:ranged, range_key: true) klass.string_attr(:other) expect(klass.hash_key).to eq(:mykey) expect(klass.range_key).to eq(:ranged) end it 'should reject assigning the same attribute as hash and range key' do expect { klass.string_attr(:oops, hash_key: true, range_key: true) }.to raise_error(ArgumentError) end end describe 'Attributes' do it 'should create dynamic methods around attributes' do klass.string_attr(:text) x = klass.new x.text = 'Hello world!' expect(x.text).to eq('Hello world!') end it 'should reject non-symbolized attribute names' do expect { klass.float_attr('floating') }.to raise_error(ArgumentError) end it 'rejects collisions of db storage names with existing attr names' do klass.string_attr(:dup_name, database_attribute_name: 'dup_storage') expect { klass.string_attr(:fail, database_attribute_name: 'dup_name') }.to raise_error(Aws::Record::Errors::NameCollision) end it 'rejects collisions of attr names with existing db storage names' do klass.string_attr(:dup_name, database_attribute_name: 'dup_storage') expect { klass.string_attr(:dup_storage, database_attribute_name: 'fail') }.to raise_error(Aws::Record::Errors::NameCollision) end it 'should not allow duplicate assignment of the same attr name' do klass.string_attr(:duplication) expect { klass.datetime_attr(:duplication) }.to raise_error( Aws::Record::Errors::NameCollision ) end it 'should typecast an integer attribute' do klass.integer_attr(:num) x = klass.new x.num = '5' expect(x.num).to eq(5) end it 'should display a hash representation of attribute raw values' do klass.string_attr(:a) klass.string_attr(:b) x = klass.new x.a = '5' x.b = 5 expect(x.to_h).to eq(a: '5', b: 5) end it 'should allow specification of a separate storage attribute name' do klass.string_attr(:a, database_attribute_name: 'column_a') klass.string_attr(:b) expect(klass.attributes.storage_name_for(:a)).to eq('column_a') expect(klass.attributes.storage_name_for(:b)).to eq('b') end it 'should reject storage name collisions' do klass.string_attr(:a, database_attribute_name: 'column_a') expect { klass.string_attr(:column_a) }.to raise_error(Errors::NameCollision) expect(klass.attributes.present?(:column_a)).to be_falsy end it 'should enforce uniqueness of storage names' do klass.string_attr(:a, database_attribute_name: 'unique') expect { klass.string_attr(:b, database_attribute_name: 'unique') }.to raise_error(Errors::NameCollision) end it 'should not allow collisions with reserved names' do expect { klass.string_attr(:to_h) }.to raise_error(Errors::ReservedName) end it 'should allow reserved names to be used as custom storage names' do klass.string_attr(:clever, database_attribute_name: 'to_h') item = klass.new item.clever = 'No problem.' expect(item.to_h).to eq(clever: 'No problem.') end end describe '#atomic_counter' do it 'should override the existing default value' do klass.string_attr(:id, hash_key: true) klass.atomic_counter(:counter, default_value: 5) item = klass.new(id: 'MyId') expect(item.counter).to eq(5) end it 'should be the existing default value' do klass.string_attr(:id, hash_key: true) klass.atomic_counter(:counter) item = klass.new(id: 'MyId') expect(item.counter).to eq(0) end it 'should be able to reassign default value after creation' do klass.string_attr(:id, hash_key: true) klass.atomic_counter(:counter, default_value: 5) item = klass.new(id: 'MyId') item.counter = 10 expect(item.counter).to eq(10) end describe '#incrementing_!' do before(:each) do klass.configure_client(client: stub_client) end let(:klass) do Class.new do include(Aws::Record) set_table_name('TestTable') integer_attr(:id, hash_key: true) atomic_counter(:counter) end end let(:api_requests) { [] } let(:stub_client) do requests = api_requests client = Aws::DynamoDB::Client.new(stub_responses: true) client.handle do |context| requests << context.params @handler.call(context) end client end it 'should increment atomic counter by default value' do stub_client.stub_responses(:update_item, attributes: { 'counter' => 1 }) item = klass.new(id: 1) item.save! item.increment_counter! expect(item.counter).to eq(1) expect(api_requests[1]). to eq( expression_attribute_names: { '#n' => 'counter' }, expression_attribute_values: { ':i' => { n: '1' } }, key: { 'id' => { n: '1' } }, return_values: 'UPDATED_NEW', table_name: 'TestTable', update_expression: 'SET #n = #n + :i' ) end it 'should increment the atomic counter by a custom value' do stub_client.stub_responses(:update_item, attributes: { 'counter' => 2 }) item = klass.new(id: 1) item.save! item.increment_counter!(2) expect(item.counter).to eq(2) expect(api_requests[1]). to eq( expression_attribute_names: { '#n' => 'counter' }, expression_attribute_values: { ':i' => { n: '2' } }, key: { 'id' => { n: '1' } }, return_values: 'UPDATED_NEW', table_name: 'TestTable', update_expression: 'SET #n = #n + :i' ) end it 'will raise when incrementing on a dirty item' do item = klass.new(id: 1) expect { item.increment_counter! }.to raise_error(Errors::RecordError) end it 'will raise when arg is not an integer' do item = klass.new(id: 1) item.save! expect { item.increment_counter!('foo') }.to raise_error(ArgumentError) end end end describe 'inheritance support' do let(:parent_model) do Class.new do include(Aws::Record) integer_attr(:id, hash_key: true) date_attr(:date, range_key: true) list_attr(:list) end end let(:child_model) do Class.new(parent_model) do include(Aws::Record) string_attr(:body) end end let(:child_model2) do Class.new(parent_model) do include(Aws::Record) string_attr(:body2) end end it 'should have instances of child models with parent attributes '\ 'and an instance of parent model with its own attributes' do parent_item = parent_model.new(id: 1, date: '2022-10-10', list: []) child_item = child_model.new(id: 2, date: '2022-10-21', list: [1, 2, 3], body: 'Hello') child_item2 = child_model2.new(id: 3, date: '2022-10-31', list: [4, 5, 6], body2: 'World') expect(parent_item.id).to eq(1) expect(parent_item.date).to eq(Date.parse('2022-10-10')) expect(parent_item.list).to eq([]) expect { parent_item.body }.to raise_error(NoMethodError) expect { parent_item.body2 }.to raise_error(NoMethodError) expect(child_item.id).to eq(2) expect(child_item.date).to eq(Date.parse('2022-10-21')) expect(child_item.list).to eq([1, 2, 3]) expect(child_item.body).to eq('Hello') expect { child_item.body2 }.to raise_error(NoMethodError) expect(child_item2.id).to eq(3) expect(child_item2.date).to eq(Date.parse('2022-10-31')) expect(child_item2.list).to eq([4, 5, 6]) expect(child_item2.body2).to eq('World') expect { child_item2.body }.to raise_error(NoMethodError) end it 'should let child model override attribute keys' do child_model.integer_attr(:rk, range_key: true) child_item = child_model.new(id: 1, rk: 1, date: '2022-10-21', list: [1, 2, 3], body: 'foo') expect(child_item.id).to eq(1) expect(child_item.rk).to eq(1) expect(child_item.key_values).to eq('id' => 1, 'rk' => 1) end it 'correctly passes default values to child model' do parent_model.string_attr(:test, default_value: -> { 'test' }) child_item = child_model.new(id: 1, date: '2022-10-21', list: [1, 2, 3], body: 'foo') expect(child_item.test).to eq('test') end end end end end