From f9a8120d8781dcae0905452bc0bba2fdb1cf8ae8 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Wed, 5 Nov 2014 15:56:06 +0200 Subject: [PATCH 1/7] Add one to one spec [skip ci] --- .../relations/association/one_to_one_spec.rb | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 spec/unit/dml/relations/association/one_to_one_spec.rb diff --git a/spec/unit/dml/relations/association/one_to_one_spec.rb b/spec/unit/dml/relations/association/one_to_one_spec.rb new file mode 100644 index 0000000..0e21718 --- /dev/null +++ b/spec/unit/dml/relations/association/one_to_one_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe Dml::Relations::Association::OneToOne do + let(:instance) do + described_class.new(source_name, target_name, options) + end + + let(:source_name) { :users } + let(:target_name) { :profile } + let(:target_relation) { :profiles } + let(:options) { Hash[] } + + describe '#type' do + subject { instance.type } + + it { expect(subject).to equal(:one_to_one) } + end + + describe '#source_relation' do + subject { instance.source_relation } + + it { expect(subject).to equal(source_name) } + end + + describe '#target_relation' do + subject { instance.target_relation } + + context 'when specified in options' do + let(:target_name) { :activity_log } + let(:target_relation) { :stats } + + let(:options) do + { target_relation: target_relation } + end + + it { expect(subject).to equal(target_relation) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(target_relation) } + end + end + + describe '#target_name' do + subject { instance.target_name } + + it { expect(subject).to equal(target_name) } + end + + describe '#foreign_key' do + subject { instance.foreign_key } + + context 'if target name and relation name are same' do + context 'when specified in options' do + let(:options) do + { foreign_key: :info_id } + end + + it { expect(subject).to equal(:info_id) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(:profile_id) } + end + end + + context 'if target name is not the same with relation name' do + let(:target_name) { :activity_log } + let(:target_relation) { :stats } + + context 'when specified in options' do + let(:options) do + { foreign_key: :activity_id } + end + + it { expect(subject).to equal(:activity_id) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(:activity_log_id) } + end + end + end + + describe '#target_key' do + subject { instance.target_key } + + context 'if target name and relation name are same' do + context 'when specified in options' do + let(:native_key) { :uuid } + + let(:options) do + { target_key: native_key } + end + + it { expect(subject).to equal(native_key) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(:id) } + end + end + + context 'if target name is not the same with relation name' do + context 'when specified in options' do + let(:native_key) { :uuid } + + let(:options) do + { target_key: native_key } + end + + it { expect(subject).to equal(native_key) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(:id) } + end + end + end + +end From 52058d37473959670fc71afdba5d15817c954d3b Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Tue, 11 Nov 2014 19:45:47 +0200 Subject: [PATCH 2/7] Basic associations --- dml.gemspec | 1 + lib/dml.rb | 3 + lib/dml/relation.rb | 9 ++ lib/dml/relation/associations/many_to_one.rb | 15 ++ lib/dml/relation/associations/one_to_one.rb | 15 ++ lib/dml/relation/associations/to_one.rb | 59 +++++++ .../relation/associations/many_to_one_spec.rb | 150 ++++++++++++++++++ .../relation/associations/one_to_one_spec.rb | 150 ++++++++++++++++++ spec/unit/dml/relation/relation_spec.rb | 18 +++ .../relations/association/one_to_one_spec.rb | 121 -------------- 10 files changed, 420 insertions(+), 121 deletions(-) create mode 100644 lib/dml/relation.rb create mode 100644 lib/dml/relation/associations/many_to_one.rb create mode 100644 lib/dml/relation/associations/one_to_one.rb create mode 100644 lib/dml/relation/associations/to_one.rb create mode 100644 spec/unit/dml/relation/associations/many_to_one_spec.rb create mode 100644 spec/unit/dml/relation/associations/one_to_one_spec.rb create mode 100644 spec/unit/dml/relation/relation_spec.rb delete mode 100644 spec/unit/dml/relations/association/one_to_one_spec.rb diff --git a/dml.gemspec b/dml.gemspec index 95d3f21..c7c0c00 100644 --- a/dml.gemspec +++ b/dml.gemspec @@ -20,6 +20,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'sequel', '~> 4.6' spec.add_dependency 'virtus' + spec.add_dependency 'inflecto' spec.add_development_dependency 'bundler', '~> 1.6' spec.add_development_dependency 'rake' diff --git a/lib/dml.rb b/lib/dml.rb index 1ca3ad7..9d33a51 100644 --- a/lib/dml.rb +++ b/lib/dml.rb @@ -1,5 +1,7 @@ require 'sequel' +require 'inflecto' + ## # Data manipulation layer # Something like data-mapper on top of Sequel @@ -8,6 +10,7 @@ module Dml end +require 'dml/relation' require 'dml/collection' require 'dml/repository' require 'dml/entity' diff --git a/lib/dml/relation.rb b/lib/dml/relation.rb new file mode 100644 index 0000000..4c4d029 --- /dev/null +++ b/lib/dml/relation.rb @@ -0,0 +1,9 @@ +module Dml + class Relation + + end +end + +require 'dml/relation/associations/to_one' +require 'dml/relation/associations/one_to_one' +require 'dml/relation/associations/many_to_one' diff --git a/lib/dml/relation/associations/many_to_one.rb b/lib/dml/relation/associations/many_to_one.rb new file mode 100644 index 0000000..3f723a9 --- /dev/null +++ b/lib/dml/relation/associations/many_to_one.rb @@ -0,0 +1,15 @@ +module Dml + class Relation + module Associations + + class ManyToOne < ToOne + + def type + :many_to_one + end + + end + + end + end +end diff --git a/lib/dml/relation/associations/one_to_one.rb b/lib/dml/relation/associations/one_to_one.rb new file mode 100644 index 0000000..19838ff --- /dev/null +++ b/lib/dml/relation/associations/one_to_one.rb @@ -0,0 +1,15 @@ +module Dml + class Relation + module Associations + + class OneToOne < ToOne + + def type + :one_to_one + end + + end + + end + end +end diff --git a/lib/dml/relation/associations/to_one.rb b/lib/dml/relation/associations/to_one.rb new file mode 100644 index 0000000..b8e55d6 --- /dev/null +++ b/lib/dml/relation/associations/to_one.rb @@ -0,0 +1,59 @@ +module Dml + class Relation + module Associations + + class ToOne + attr_reader :source_relation + + attr_reader :target_name + + attr_reader :target_relation + + attr_reader :foreign_keys + + attr_reader :target_keys + + attr_reader :reference_keys + + attr_reader :options + + private + + def initialize(source_name, target_name, options={}) + @source_relation = source_name + @target_name = target_name + + @options = options + + set_target_relation + set_foreign_keys + set_target_keys + set_reference_keys + + freeze + end + + def set_target_relation + @target_relation = if options.has_key?(:target_relation) + options[:target_relation] + else + Inflecto.pluralize(target_name) + end.to_sym + end + + def set_foreign_keys + @foreign_keys = Array(options[:foreign_keys] || "#{target_name}_id").map(&:to_sym) + end + + def set_target_keys + @target_keys = Array(options[:target_keys] || :id).map(&:to_sym) + end + + def set_reference_keys + @reference_keys = Hash[foreign_keys.zip(target_keys)] + end + end + + end + end +end diff --git a/spec/unit/dml/relation/associations/many_to_one_spec.rb b/spec/unit/dml/relation/associations/many_to_one_spec.rb new file mode 100644 index 0000000..891974f --- /dev/null +++ b/spec/unit/dml/relation/associations/many_to_one_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe Dml::Relation::Associations::ManyToOne do + let(:instance) do + described_class.new(source_name, target_name, options) + end + + let(:source_name) { :users } + let(:target_name) { :company } + let(:target_relation) { :companies } + let(:options) { Hash[] } + + describe '#type' do + subject { instance.type } + + it { expect(subject).to equal(:many_to_one) } + end + + describe '#source_relation' do + subject { instance.source_relation } + + it { expect(subject).to equal(source_name) } + end + + describe '#target_relation' do + subject { instance.target_relation } + + context 'when specified in options' do + let(:target_name) { :motherland } + let(:target_relation) { :countries } + + let(:options) do + { target_relation: target_relation } + end + + it { expect(subject).to equal(target_relation) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(target_relation) } + end + end + + describe '#target_name' do + subject { instance.target_name } + + it { expect(subject).to equal(target_name) } + end + + describe '#foreign_keys' do + subject { instance.foreign_keys } + + context 'if target name and relation name are same' do + context 'when specified in options' do + let(:options) do + { foreign_keys: fk } + end + + context 'single key' do + let(:fk) { :motherland_id } + + it { expect(subject).to contain_exactly(fk) } + end + + context 'multiple key' do + let(:fk) { [:motherland_id, :zip] } + + it { expect(subject).to match_array(fk) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:company_id) } + end + end + + context 'if target name is not the same with relation name' do + let(:target_name) { :division } + let(:target_relation) { :regions } + + context 'when specified in options' do + let(:options) do + { foreign_keys: fk } + end + + context 'single key' do + let(:fk) { :region_code } + + it { expect(subject).to contain_exactly(fk) } + end + + context 'multiple keys' do + let(:fk) { [:region_code, :zip] } + + it { expect(subject).to match_array(fk) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:division_id) } + end + end + end + + describe '#target_keys' do + subject { instance.target_keys } + + context 'when specified in options' do + let(:options) do + { target_keys: native_key } + end + + context 'single key' do + let(:native_key) { :uuid } + + it { expect(subject).to match_array(native_key) } + end + + context 'multiple keys' do + let(:native_key) { [:uuid, :type] } + + it { expect(subject).to match_array(native_key) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:id) } + end + end + + describe '#reference_keys' do + subject { instance.reference_keys } + + context 'when one key' do + it { expect(subject).to match(:company_id => :id) } + end + + context 'when many keys' do + let(:options) do + { + foreign_keys: [:company_id, :company_country], + target_keys: [:id, :country_id] + } + end + + it { expect(subject).to match({ :company_id => :id, :company_country => :country_id }) } + end + end + +end diff --git a/spec/unit/dml/relation/associations/one_to_one_spec.rb b/spec/unit/dml/relation/associations/one_to_one_spec.rb new file mode 100644 index 0000000..fa36816 --- /dev/null +++ b/spec/unit/dml/relation/associations/one_to_one_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe Dml::Relation::Associations::OneToOne do + let(:instance) do + described_class.new(source_name, target_name, options) + end + + let(:source_name) { :users } + let(:target_name) { :profile } + let(:target_relation) { :profiles } + let(:options) { Hash[] } + + describe '#type' do + subject { instance.type } + + it { expect(subject).to equal(:one_to_one) } + end + + describe '#source_relation' do + subject { instance.source_relation } + + it { expect(subject).to equal(source_name) } + end + + describe '#target_relation' do + subject { instance.target_relation } + + context 'when specified in options' do + let(:target_name) { :activity_log } + let(:target_relation) { :stats } + + let(:options) do + { target_relation: target_relation } + end + + it { expect(subject).to equal(target_relation) } + end + + context 'when not specified in options' do + it { expect(subject).to equal(target_relation) } + end + end + + describe '#target_name' do + subject { instance.target_name } + + it { expect(subject).to equal(target_name) } + end + + describe '#foreign_keys' do + subject { instance.foreign_keys } + + context 'if target name and relation name are same' do + context 'when specified in options' do + let(:options) do + { foreign_keys: fk } + end + + context 'single key' do + let(:fk) { :info_id } + + it { expect(subject).to contain_exactly(fk) } + end + + context 'multiple keys' do + let(:fk) { [:info_id, :company_id] } + + it { expect(subject).to match_array(fk) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:profile_id) } + end + end + + context 'if target name is not the same with relation name' do + let(:target_name) { :activity_log } + let(:target_relation) { :stats } + + context 'when specified in options' do + context 'single key' do + let(:options) do + { foreign_keys: :activity_id } + end + + it { expect(subject).to contain_exactly(:activity_id) } + end + + context 'multiple keys' do + let(:options) do + { foreign_keys: [:activity_id, :activity_type] } + end + + it { expect(subject).to contain_exactly(:activity_id, :activity_type) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:activity_log_id) } + end + end + end + + describe '#target_keys' do + subject { instance.target_keys } + + context 'when specified in options' do + let(:options) do + { target_keys: native_key } + end + + context 'single key' do + let(:native_key) { :uuid } + + it { expect(subject).to match_array(native_key) } + end + + context 'multiple keys' do + let(:native_key) { [:uuid, :type] } + + it { expect(subject).to match_array(native_key) } + end + end + + context 'when not specified in options' do + it { expect(subject).to contain_exactly(:id) } + end + end + + describe '#reference_keys' do + subject { instance.reference_keys } + + context 'when one key' do + it { expect(subject).to match(:profile_id => :id) } + end + + context 'when many keys' do + let(:options) do + { + foreign_keys: [:profile_id, :profile_type], + target_keys: [:id, :type] + } + end + + it { expect(subject).to match({ :profile_id => :id, :profile_type => :type }) } + end + end + +end diff --git a/spec/unit/dml/relation/relation_spec.rb b/spec/unit/dml/relation/relation_spec.rb new file mode 100644 index 0000000..2d5eac4 --- /dev/null +++ b/spec/unit/dml/relation/relation_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Dml::Relations::Relation do + let(:instance) { described_class.new(name) } + + describe '#name' do + + end + + describe '#primary_key' do + + end + + describe '#associations' do + + end + +end diff --git a/spec/unit/dml/relations/association/one_to_one_spec.rb b/spec/unit/dml/relations/association/one_to_one_spec.rb deleted file mode 100644 index 0e21718..0000000 --- a/spec/unit/dml/relations/association/one_to_one_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -require 'spec_helper' - -describe Dml::Relations::Association::OneToOne do - let(:instance) do - described_class.new(source_name, target_name, options) - end - - let(:source_name) { :users } - let(:target_name) { :profile } - let(:target_relation) { :profiles } - let(:options) { Hash[] } - - describe '#type' do - subject { instance.type } - - it { expect(subject).to equal(:one_to_one) } - end - - describe '#source_relation' do - subject { instance.source_relation } - - it { expect(subject).to equal(source_name) } - end - - describe '#target_relation' do - subject { instance.target_relation } - - context 'when specified in options' do - let(:target_name) { :activity_log } - let(:target_relation) { :stats } - - let(:options) do - { target_relation: target_relation } - end - - it { expect(subject).to equal(target_relation) } - end - - context 'when not specified in options' do - it { expect(subject).to equal(target_relation) } - end - end - - describe '#target_name' do - subject { instance.target_name } - - it { expect(subject).to equal(target_name) } - end - - describe '#foreign_key' do - subject { instance.foreign_key } - - context 'if target name and relation name are same' do - context 'when specified in options' do - let(:options) do - { foreign_key: :info_id } - end - - it { expect(subject).to equal(:info_id) } - end - - context 'when not specified in options' do - it { expect(subject).to equal(:profile_id) } - end - end - - context 'if target name is not the same with relation name' do - let(:target_name) { :activity_log } - let(:target_relation) { :stats } - - context 'when specified in options' do - let(:options) do - { foreign_key: :activity_id } - end - - it { expect(subject).to equal(:activity_id) } - end - - context 'when not specified in options' do - it { expect(subject).to equal(:activity_log_id) } - end - end - end - - describe '#target_key' do - subject { instance.target_key } - - context 'if target name and relation name are same' do - context 'when specified in options' do - let(:native_key) { :uuid } - - let(:options) do - { target_key: native_key } - end - - it { expect(subject).to equal(native_key) } - end - - context 'when not specified in options' do - it { expect(subject).to equal(:id) } - end - end - - context 'if target name is not the same with relation name' do - context 'when specified in options' do - let(:native_key) { :uuid } - - let(:options) do - { target_key: native_key } - end - - it { expect(subject).to equal(native_key) } - end - - context 'when not specified in options' do - it { expect(subject).to equal(:id) } - end - end - end - -end From 700e2b8a43f65ad6b5a0fdb8ef05a1ee18853b69 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Tue, 11 Nov 2014 22:45:31 +0200 Subject: [PATCH 3/7] Basic relation object --- lib/dml/relation.rb | 31 ++++++++++++++++++++ lib/dml/relation/associations/to_one.rb | 9 ++++++ spec/unit/dml/relation/relation_spec.rb | 18 ------------ spec/unit/dml/relation_spec.rb | 39 +++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 18 deletions(-) delete mode 100644 spec/unit/dml/relation/relation_spec.rb create mode 100644 spec/unit/dml/relation_spec.rb diff --git a/lib/dml/relation.rb b/lib/dml/relation.rb index 4c4d029..f056c2e 100644 --- a/lib/dml/relation.rb +++ b/lib/dml/relation.rb @@ -1,6 +1,37 @@ module Dml class Relation + attr_reader :name + + attr_reader :primary_key + + attr_reader :associations + + attr_reader :dependencies + + def freeze + @associations.freeze + @dependencies.freeze + + super + end + + private + + def initialize(name, pk=nil, associations={}) + @name = name + @primary_key = pk.kind_of?(Array) ? pk.map(&:to_sym) : pk.to_sym + @associations = associations + + @dependencies = Set[] + + associations.values.each do |assoc| + dependencies.add(assoc.target_relation) + end + + freeze + end + end end diff --git a/lib/dml/relation/associations/to_one.rb b/lib/dml/relation/associations/to_one.rb index b8e55d6..15865dc 100644 --- a/lib/dml/relation/associations/to_one.rb +++ b/lib/dml/relation/associations/to_one.rb @@ -17,6 +17,15 @@ class ToOne attr_reader :options + def freeze + @options.freeze + @foreign_keys.freeze + @target_keys.freeze + @reference_keys.freeze + + super + end + private def initialize(source_name, target_name, options={}) diff --git a/spec/unit/dml/relation/relation_spec.rb b/spec/unit/dml/relation/relation_spec.rb deleted file mode 100644 index 2d5eac4..0000000 --- a/spec/unit/dml/relation/relation_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' - -describe Dml::Relations::Relation do - let(:instance) { described_class.new(name) } - - describe '#name' do - - end - - describe '#primary_key' do - - end - - describe '#associations' do - - end - -end diff --git a/spec/unit/dml/relation_spec.rb b/spec/unit/dml/relation_spec.rb new file mode 100644 index 0000000..6a6a501 --- /dev/null +++ b/spec/unit/dml/relation_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Dml::Relation do + let(:instance) { described_class.new(name, pkey, associations) } + + let(:name) { :users } + let(:pkey) { :id } + let(:associations) do + { + company: instance_double('Dml::Relation::Associations::ManyToOne', target_relation: :companies), + profile: instance_double('Dml::Relation::Associations::OneToOne', target_relation: :profiles) + } + end + + describe '#name' do + subject { instance.name } + + it { expect(subject).to equal(name) } + end + + describe '#primary_key' do + subject { instance.primary_key } + + it { expect(subject).to equal(pkey) } + end + + describe '#associations' do + subject { instance.associations } + + it { expect(subject).to match(associations) } + end + + describe '#dependencies' do + subject { instance.dependencies } + + it { expect(subject).to contain_exactly(:companies, :profiles) } + end + +end From d669be765db8f8ef2dbecdc3e2e2feda4a04dd93 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Wed, 12 Nov 2014 20:17:06 +0200 Subject: [PATCH 4/7] Add relations list --- lib/dml/relation.rb | 2 ++ lib/dml/relation/list.rb | 54 +++++++++++++++++++++++++++++ spec/unit/dml/relation/list_spec.rb | 34 ++++++++++++++++++ spec/unit/dml/relation_spec.rb | 12 ++++++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 lib/dml/relation/list.rb create mode 100644 spec/unit/dml/relation/list_spec.rb diff --git a/lib/dml/relation.rb b/lib/dml/relation.rb index f056c2e..062db6a 100644 --- a/lib/dml/relation.rb +++ b/lib/dml/relation.rb @@ -35,6 +35,8 @@ def initialize(name, pk=nil, associations={}) end end +require 'dml/relation/list' + require 'dml/relation/associations/to_one' require 'dml/relation/associations/one_to_one' require 'dml/relation/associations/many_to_one' diff --git a/lib/dml/relation/list.rb b/lib/dml/relation/list.rb new file mode 100644 index 0000000..f605128 --- /dev/null +++ b/lib/dml/relation/list.rb @@ -0,0 +1,54 @@ +require 'tsort' + +module Dml + class Relation + class List + + class Sorter + include TSort + + attr_reader :result + + private + + def initialize(relations) + @relations = relations.each_with_object({}) { |r, h| h[r.name] = r } + + @result = {} + + tsort_each do |item| + @result[item] = @relations[item] + end + end + + def tsort_each_node(&block) + @relations.keys.each(&block) + end + + def tsort_each_child(node, &block) + @relations[node].dependencies.each(&block) + end + end + + attr_reader :names + + def get(key) + @relations[key] + end + + def to_a + @array + end + + private + + def initialize(relations) + sorter = Sorter.new(relations) + + @relations = sorter.result.freeze + @names = @relations.keys.freeze + @array = @relations.values.freeze + end + end + end +end diff --git a/spec/unit/dml/relation/list_spec.rb b/spec/unit/dml/relation/list_spec.rb new file mode 100644 index 0000000..50943f0 --- /dev/null +++ b/spec/unit/dml/relation/list_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Dml::Relation::List do + let(:instance) { described_class.new(relations) } + + let(:relations) do + [ + instance_double('Dml::Relation', name: :users, dependencies: Set[:companies, :locations]), + instance_double('Dml::Relation', name: :companies, dependencies: Set[]), + instance_double('Dml::Relation', name: :locations, dependencies: Set[:companies]) + ] + end + + describe '#get' do + subject { instance.get(rel_name) } + + let(:rel_name) { :users } + + it { expect(subject.name).to equal(rel_name) } + end + + describe '#names' do + subject { instance.names } + + it { expect(subject).to eql %i(companies locations users) } + end + + describe '#to_a' do + subject { instance.to_a } + + it { expect(subject.map(&:name)).to eql %i(companies locations users) } + end + +end diff --git a/spec/unit/dml/relation_spec.rb b/spec/unit/dml/relation_spec.rb index 6a6a501..c96f618 100644 --- a/spec/unit/dml/relation_spec.rb +++ b/spec/unit/dml/relation_spec.rb @@ -33,7 +33,17 @@ describe '#dependencies' do subject { instance.dependencies } - it { expect(subject).to contain_exactly(:companies, :profiles) } + context 'when associations was set' do + it { expect(subject).to contain_exactly(:companies, :profiles) } + end + + context 'when without associations' do + let(:associations) { {} } + + it { expect(subject).to be_kind_of(Set) } + it { expect(subject).to be_empty } + end + end end From 33b0e0d3624587bf988dbd20765b0a036dcdc849 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Wed, 12 Nov 2014 22:45:12 +0200 Subject: [PATCH 5/7] Add some docs --- lib/dml/relation/associations/to_one.rb | 87 +++++++++++++++++++++++++ lib/dml/relation/list.rb | 21 +++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/lib/dml/relation/associations/to_one.rb b/lib/dml/relation/associations/to_one.rb index 15865dc..98a81db 100644 --- a/lib/dml/relation/associations/to_one.rb +++ b/lib/dml/relation/associations/to_one.rb @@ -3,20 +3,80 @@ class Relation module Associations class ToOne + + ## + # Source relation name + # + # Returns: {Symbol} + # attr_reader :source_relation + ## + # Association name + # + # Returns: {Symbol} + # attr_reader :target_name + ## + # Associated relation name + # + # Returns: {Symbol} + # attr_reader :target_relation + ## + # Source foreign key list + # + # Example: + # + # association.foreign_keys #=> [:company_id, :company_country] + # + # Returns: {Array(Symbol)} + # attr_reader :foreign_keys + ## + # Associated relation primary keys list + # + # Example: + # + # association.target_keys #=> [:id, :country_id] + # + # Returns: {Array(Symbol)} + # attr_reader :target_keys + ## + # Foreign key => primary key list + # + # Example: + # + # association.reference_keys #=> { :company_id => :id, :company_country => :country_id } + # + # Returns: {Hash} + # attr_reader :reference_keys + ## + # Association options + # + # Returns: {Hash} + # attr_reader :options + ## + # Abstract: Association type + # + # Returns: {Symbol} + # + def type + fail(NotImplementedError) + end + + ## + # Deeply freeze object + # def freeze @options.freeze @foreign_keys.freeze @@ -28,6 +88,17 @@ def freeze private + ## + # Constructor: + # + # Params: + # - source_name {Symbol} source relation name + # - target_name {Symbol} association name + # - options {Hash} + # - target_relation {Symbol|String} target relation name + # - foreign_keys {Symbol|Array} source foreign key(s) + # - target_keys {Symbol|Array} target primary key(s) + # def initialize(source_name, target_name, options={}) @source_relation = source_name @target_name = target_name @@ -42,6 +113,12 @@ def initialize(source_name, target_name, options={}) freeze end + ## + # Private: set target relation name + # + # If name given in options - take it unchanged, + # otherwise – pluralize association name + # def set_target_relation @target_relation = if options.has_key?(:target_relation) options[:target_relation] @@ -50,14 +127,24 @@ def set_target_relation end.to_sym end + ## + # Private: set source relation foreign key list + # def set_foreign_keys @foreign_keys = Array(options[:foreign_keys] || "#{target_name}_id").map(&:to_sym) end + ## + # Private: set target relation primary key list + # def set_target_keys @target_keys = Array(options[:target_keys] || :id).map(&:to_sym) end + ## + # Private: create hash where keys is a source foreign keys + # and values is a target primary keys + # def set_reference_keys @reference_keys = Hash[foreign_keys.zip(target_keys)] end diff --git a/lib/dml/relation/list.rb b/lib/dml/relation/list.rb index f605128..51cfdc4 100644 --- a/lib/dml/relation/list.rb +++ b/lib/dml/relation/list.rb @@ -30,23 +30,40 @@ def tsort_each_child(node, &block) end end + ## + # Returns: {Array(Symbol)} ordered relation names + # attr_reader :names + ## + # Get relation by name + # + # Returns: {Dml::Relation} + # def get(key) - @relations[key] + @relations[key.to_sym] end + ## + # Returns: {Array(Dml::Relation)} ordered relations + # def to_a @array end private + ## + # Constructor: + # + # Params: + # - relations {Array(Dml::Relation)} + # def initialize(relations) sorter = Sorter.new(relations) @relations = sorter.result.freeze - @names = @relations.keys.freeze + @names = @relations.keys.freeze @array = @relations.values.freeze end end From 1119ce984d6474d24e8b2e59c7a4fa37eca1912c Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Thu, 27 Nov 2014 07:26:36 +0200 Subject: [PATCH 6/7] Add relations dsl --- Gemfile | 1 + dml.gemspec | 2 +- lib/dml.rb | 7 +- lib/dml/relation.rb | 3 + lib/dml/relation/dsl.rb | 92 ++++++++++++++++ lib/dml/relation/list.rb | 7 +- spec/integration/dml/relation_dsl_spec.rb | 87 +++++++++++++++ spec/shared_examples/relation/dsl_assocs.rb | 53 +++++++++ .../dml/relation/dsl/belongs_to_one_spec.rb | 9 ++ spec/unit/dml/relation/dsl/belongs_to_spec.rb | 9 ++ spec/unit/dml/relation/dsl/relation_spec.rb | 101 ++++++++++++++++++ spec/unit/dml/relation/list_spec.rb | 17 +++ 12 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 lib/dml/relation/dsl.rb create mode 100644 spec/integration/dml/relation_dsl_spec.rb create mode 100644 spec/shared_examples/relation/dsl_assocs.rb create mode 100644 spec/unit/dml/relation/dsl/belongs_to_one_spec.rb create mode 100644 spec/unit/dml/relation/dsl/belongs_to_spec.rb create mode 100644 spec/unit/dml/relation/dsl/relation_spec.rb diff --git a/Gemfile b/Gemfile index 6e8fc11..d12ec34 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,4 @@ source 'https://rubygems.org' gemspec gem 'sequelize', github: 'Ptico/sequelize' +gem 'pry' diff --git a/dml.gemspec b/dml.gemspec index c7c0c00..6c56f9f 100644 --- a/dml.gemspec +++ b/dml.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.6' spec.add_development_dependency 'rake' - spec.add_development_dependency 'rspec', '~> 3.0.0' + spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'sequel_pg' end diff --git a/lib/dml.rb b/lib/dml.rb index 9d33a51..ae68cd8 100644 --- a/lib/dml.rb +++ b/lib/dml.rb @@ -2,15 +2,18 @@ require 'inflecto' +require 'dml/relation' + ## # Data manipulation layer # Something like data-mapper on top of Sequel # module Dml - + def self.define_relations(&block) + @relations = Relation::DSL.new(&block) + end end -require 'dml/relation' require 'dml/collection' require 'dml/repository' require 'dml/entity' diff --git a/lib/dml/relation.rb b/lib/dml/relation.rb index 062db6a..3064f8f 100644 --- a/lib/dml/relation.rb +++ b/lib/dml/relation.rb @@ -1,5 +1,6 @@ module Dml class Relation + UndefinedDependencyError = Class.new(StandardError) attr_reader :name @@ -40,3 +41,5 @@ def initialize(name, pk=nil, associations={}) require 'dml/relation/associations/to_one' require 'dml/relation/associations/one_to_one' require 'dml/relation/associations/many_to_one' + +require 'dml/relation/dsl' diff --git a/lib/dml/relation/dsl.rb b/lib/dml/relation/dsl.rb new file mode 100644 index 0000000..df1a536 --- /dev/null +++ b/lib/dml/relation/dsl.rb @@ -0,0 +1,92 @@ +module Dml + class Relation + + class DSL + class Relation + attr_reader :associations + + def key(*names) + @key = names + end + + def belongs_to(association, &block) + assoc_name = association.to_sym + @associations[assoc_name] = BelongsTo.new(@name, assoc_name, &block).association + end + + def belongs_to_one(association, &block) + assoc_name = association.to_sym + @associations[assoc_name] = BelongsToOne.new(@name, assoc_name, &block).association + end + + def relation + Dml::Relation.new(@name, @key, @associations) + end + + private + + def initialize(name, &block) + @name = name.to_sym + @key = [:id] + @associations = {} + + instance_eval(&block) if block_given? + end + end + + class BelongsTo + def relation(name) + @options[:target_relation] = name + end + + def foreign_key(*keys) + @options[:foreign_keys] = keys + end + + def reference_key(*keys) + @options[:target_keys] = keys + end + + def association + klass.new(@source_name, @target_name, @options) + end + + private + + def initialize(source_name, target_name, &block) + @source_name = source_name + @target_name = target_name + + @options = {} + + instance_eval(&block) if block_given? + end + + def klass + Dml::Relation::Associations::ManyToOne + end + end + + class BelongsToOne < BelongsTo + def klass + Dml::Relation::Associations::OneToOne + end + end + + attr_reader :result + + def relation(name, &block) + @relations << Relation.new(name, &block).relation + end + + private + + def initialize(&block) + @relations = [] + instance_eval(&block) if block_given? + @result = Dml::Relation::List.new(@relations) + end + end + + end +end diff --git a/lib/dml/relation/list.rb b/lib/dml/relation/list.rb index 51cfdc4..abb8b26 100644 --- a/lib/dml/relation/list.rb +++ b/lib/dml/relation/list.rb @@ -26,7 +26,11 @@ def tsort_each_node(&block) end def tsort_each_child(node, &block) - @relations[node].dependencies.each(&block) + if rel = @relations[node] + rel.dependencies.each(&block) + else + fail(UndefinedDependencyError, "relation `#{node}` does not specified in relation list") + end end end @@ -43,6 +47,7 @@ def tsort_each_child(node, &block) def get(key) @relations[key.to_sym] end + alias :[] :get ## # Returns: {Array(Dml::Relation)} ordered relations diff --git a/spec/integration/dml/relation_dsl_spec.rb b/spec/integration/dml/relation_dsl_spec.rb new file mode 100644 index 0000000..a6de501 --- /dev/null +++ b/spec/integration/dml/relation_dsl_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe 'Relations DSL' do + let(:relations) do + Dml::Relation::DSL.new do + relation :districts do + key :code + end + + relation :countries do + key :id + end + + relation :users do + key :id + + belongs_to(:company) + belongs_to_one(:profile) + belongs_to_one(:activity_log) do + relation :stats + end + end + + relation :profiles + relation :stats + + relation :companies do + key :id + + belongs_to(:region) do + relation :districts + foreign_key :region_code + reference_key :code + end + end + end + end + + let(:result) { relations.result } + + describe 'relation list' do + it 'should be sorted' do + expect(result.names).to eql([:districts, :countries, :companies, :profiles, :stats, :users]) + end + end + + describe 'relation' do + subject { result.get(:profiles) } + + it 'should set the name' do + expect(subject.name).to equal(:profiles) + end + + it 'should set foreign key' do + expect(subject.primary_key).to equal(:id) + end + end + + describe 'association list' do + subject { result.get(:users) } + + it 'should set all associations' do + expect(subject.dependencies).to contain_exactly(:companies, :profiles, :stats) + end + + it 'should set dependencies' do + expect(subject.associations.keys).to contain_exactly(:company, :profile, :activity_log) + end + end + + describe 'association' do + subject { result[:companies].associations[:region] } + + it 'should set relation name' do + expect(subject.target_relation).to equal(:districts) + end + + it 'should set foreign key' do + expect(subject.foreign_keys).to contain_exactly(:region_code) + end + + it 'should set reference key' do + expect(subject.target_keys).to contain_exactly(:code) + end + end + +end diff --git a/spec/shared_examples/relation/dsl_assocs.rb b/spec/shared_examples/relation/dsl_assocs.rb new file mode 100644 index 0000000..3c7fd74 --- /dev/null +++ b/spec/shared_examples/relation/dsl_assocs.rb @@ -0,0 +1,53 @@ +RSpec.shared_context 'dsl association' do + before(:each) do + allow(klass).to receive(:new) + end + + after(:each) do + instance.association + end + + describe '#relation' do + before(:each) do + instance.relation(:profiles) + end + + it { expect(klass).to receive(:new).with(:users, :info, { :target_relation => :profiles }) } + end + + describe '#foreign_key' do + context 'when single' do + before(:each) do + instance.foreign_key(:uid) + end + + it { expect(klass).to receive(:new).with(:users, :info, { :foreign_keys => [:uid] }) } + end + + context 'when composite' do + before(:each) do + instance.foreign_key(:id, :user_id) + end + + it { expect(klass).to receive(:new).with(:users, :info, { :foreign_keys => [:id, :user_id] }) } + end + end + + describe '#reference_key' do + context 'when single' do + before(:each) do + instance.reference_key(:uid) + end + + it { expect(klass).to receive(:new).with(:users, :info, { :target_keys => [:uid] }) } + end + + context 'when composite' do + before(:each) do + instance.reference_key(:id, :user_id) + end + + it { expect(klass).to receive(:new).with(:users, :info, { :target_keys => [:id, :user_id] }) } + end + end +end diff --git a/spec/unit/dml/relation/dsl/belongs_to_one_spec.rb b/spec/unit/dml/relation/dsl/belongs_to_one_spec.rb new file mode 100644 index 0000000..0c4c7d6 --- /dev/null +++ b/spec/unit/dml/relation/dsl/belongs_to_one_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Dml::Relation::DSL::BelongsToOne do + let(:instance) { described_class.new(:users, :info) } + + let(:klass) { Dml::Relation::Associations::OneToOne } + + it_behaves_like 'dsl association' +end diff --git a/spec/unit/dml/relation/dsl/belongs_to_spec.rb b/spec/unit/dml/relation/dsl/belongs_to_spec.rb new file mode 100644 index 0000000..3e3e6fe --- /dev/null +++ b/spec/unit/dml/relation/dsl/belongs_to_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Dml::Relation::DSL::BelongsTo do + let(:instance) { described_class.new(:users, :info) } + + let(:klass) { Dml::Relation::Associations::ManyToOne } + + it_behaves_like 'dsl association' +end diff --git a/spec/unit/dml/relation/dsl/relation_spec.rb b/spec/unit/dml/relation/dsl/relation_spec.rb new file mode 100644 index 0000000..838d231 --- /dev/null +++ b/spec/unit/dml/relation/dsl/relation_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe Dml::Relation::DSL::Relation do + let(:instance) { described_class.new(name) } + let(:name) { :users } + + describe '#key' do + before(:each) do + allow(Dml::Relation).to receive(:new) + end + + after(:each) do + instance.relation + end + + context 'by default' do + it 'should be :id' do + expect(Dml::Relation).to receive(:new).with(name, [:id], {}) + end + end + + context 'when single' do + before(:each) do + instance.key(:uid) + end + + it { expect(Dml::Relation).to receive(:new).with(name, [:uid], {}) } + end + + context 'when composite' do + before(:each) do + instance.key(:id, :company) + end + + it { expect(Dml::Relation).to receive(:new).with(name, [:id, :company], {}) } + end + end + + describe '#belongs_to' do + let(:assoc) { instance_double('Dml::Relation::Associations::ManyToOne') } + let(:dsl) { class_double('Dml::Relation::DSL::BelongsTo') } + let(:dsl_instance) { instance_double('Dml::Relation::DSL::BelongsTo', association: assoc) } + let(:blk) { ->(foo) { foo } } + + before(:each) do + allow(dsl).to receive(:new).and_return(dsl_instance) + stub_const('Dml::Relation::DSL::BelongsTo', dsl) + end + + it 'adds association to associations list' do + instance.belongs_to(:user) + expect(instance.associations).to include(user: assoc) + end + + it 'sends name and params to DSL' do + expect(dsl).to receive(:new).with(name, :user).and_yield(blk).and_return(dsl_instance) + instance.belongs_to(:user, &blk) + end + end + + describe '#belongs_to_one' do + let(:assoc) { instance_double('Dml::Relation::Associations::OneToOne') } + let(:dsl) { class_double('Dml::Relation::DSL::BelongsToOne') } + let(:dsl_instance) { instance_double('Dml::Relation::DSL::BelongsToOne', association: assoc) } + let(:blk) { ->(foo) { foo } } + + before(:each) do + allow(dsl).to receive(:new).and_return(dsl_instance) + stub_const('Dml::Relation::DSL::BelongsToOne', dsl) + end + + it 'adds association to associations list' do + instance.belongs_to_one(:profile) + expect(instance.associations).to include(profile: assoc) + end + + it 'sends name and params to DSL' do + expect(dsl).to receive(:new).with(name, :profile).and_yield(blk).and_return(dsl_instance) + instance.belongs_to_one(:profile, &blk) + end + end + + describe '#relation' do + let(:relation_instance) { instance_double('Dml::Relation') } + + before(:each) do + allow(Dml::Relation).to receive(:new).and_return(relation_instance) + end + + it 'should return relation' do + expect(instance.relation).to equal(relation_instance) + end + + it 'should send params to relation' do + expect(Dml::Relation).to receive(:new).with(name, [:id], instance.associations) + instance.belongs_to_one(:profile) + instance.relation + end + end + +end diff --git a/spec/unit/dml/relation/list_spec.rb b/spec/unit/dml/relation/list_spec.rb index 50943f0..441deca 100644 --- a/spec/unit/dml/relation/list_spec.rb +++ b/spec/unit/dml/relation/list_spec.rb @@ -31,4 +31,21 @@ it { expect(subject.map(&:name)).to eql %i(companies locations users) } end + describe '#initialize' do + context 'when target dependency is not set' do + let(:relations) do + [ + instance_double('Dml::Relation', name: :locations, dependencies: Set[:companies]) + ] + end + + it 'should raise error if relation was not defined' do + expect { instance }.to raise_error( + Dml::Relation::UndefinedDependencyError, + 'relation `companies` does not specified in relation list' + ) + end + end + end + end From f16147887075c5d9adb0bf234a43f7e71000e7d9 Mon Sep 17 00:00:00 2001 From: Andrey Savchenko Date: Mon, 1 Dec 2014 17:17:05 +0200 Subject: [PATCH 7/7] Extract persist processor --- .rspec | 1 - lib/dml/repository/base.rb | 4 +- lib/dml/repository/persist_processor.rb | 57 +++++++++++ .../repository/postgres/persist_processor.rb | 43 ++++++++ lib/dml/repository/resource.rb | 52 +++------- spec/integration/dml/relation_dsl_spec.rb | 2 +- .../dml/repository/persist_processor_spec.rb | 99 +++++++++++++++++++ 7 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 lib/dml/repository/persist_processor.rb create mode 100644 lib/dml/repository/postgres/persist_processor.rb create mode 100644 spec/unit/dml/repository/persist_processor_spec.rb diff --git a/.rspec b/.rspec index 0d786ba..83e16f8 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,2 @@ --color ---warnings --require spec_helper diff --git a/lib/dml/repository/base.rb b/lib/dml/repository/base.rb index 2863d70..66444ad 100644 --- a/lib/dml/repository/base.rb +++ b/lib/dml/repository/base.rb @@ -25,8 +25,8 @@ def default_query(&block) end def on_persist(&block) - @on_write_block = block if block_given? - + @on_write_block ||= [] + @on_write_block << block if block_given? @on_write_block end end diff --git a/lib/dml/repository/persist_processor.rb b/lib/dml/repository/persist_processor.rb new file mode 100644 index 0000000..7d38003 --- /dev/null +++ b/lib/dml/repository/persist_processor.rb @@ -0,0 +1,57 @@ +module Dml + module Repository + class PersistProcessor + + Update = Struct.new(:pkeys, :data).freeze + + def process_insert(entities) + entities.map do |entity| + process_entity(entity) + end + end + + def process_update(entities) + pkeys = [] + + data = entities.map do |entity| + pkeys << extract_keys(entity) + process_entity(entity) + end + + Update.new(pkeys, data) + end + + def process_entity(entity) + run_callbacks(entity.attributes.dup) + end + + private + + def initialize(relation_name, primary_keys, callbacks) + @relation_name = relation_name + @primary_keys = primary_keys + @callbacks = callbacks + end + + def run_callbacks(attrs) + @callbacks.each do |callback| + callback.call(attrs) + end + attrs + end + + ## + # Private: extract primary keys from records + # + # Params: + # - record {Entity} entity + # + # Returns: {Hash} primary keys with their values from records + # + def extract_keys(entity) + Hash[@primary_keys.zip(entity.attributes.values_at(*@primary_keys))] + end + + end + end +end diff --git a/lib/dml/repository/postgres/persist_processor.rb b/lib/dml/repository/postgres/persist_processor.rb new file mode 100644 index 0000000..1566671 --- /dev/null +++ b/lib/dml/repository/postgres/persist_processor.rb @@ -0,0 +1,43 @@ +module Dml + module Repository + module Postgres + class PersistProcessor < Dml::Repository::PersistProcessor + + def process_entity(entity) + attrs = remove_pk(entity.attributes) + run_callbacks(attrs) + hstore_keys(attrs) + end + + private + + ## + # Private: deletes primary key if it not composite + # + # Params: + # - params {Hash} attributes of entity + # + # Examples: + # + # remove_pk({ id: 3, name: 'name' }) # => {name: 'name'} + # + # Returns: {Hash} cleared attributes of entity + # + def remove_pk(attrs) + if @primary_keys.size > 1 + attrs + else + attrs.reject { |key| @primary_keys.include?(key) } + end + end + + def hstore_keys(attrs) + attrs.each do |k, v| + attrs[k] = Sequel.hstore(v) if v.is_a?(Hash) + end + end + + end + end + end +end diff --git a/lib/dml/repository/resource.rb b/lib/dml/repository/resource.rb index b16d460..15ed6f8 100644 --- a/lib/dml/repository/resource.rb +++ b/lib/dml/repository/resource.rb @@ -1,3 +1,6 @@ +require 'dml/repository/persist_processor' +require 'dml/repository/postgres/persist_processor' + module Dml module Repository ## @@ -12,6 +15,10 @@ class << self attr_reader :queries + def persist_processor + @persist_processor ||= Postgres::PersistProcessor.new(relation, primary_key, on_persist) + end + ## # Static: get entity by primary key # @@ -38,25 +45,14 @@ def fetch(id) # Returns: {Array(Entity)} array of inserted items # def insert(records) - records_array = Array(records) + records = Array(records) - attr_mapper = on_persist - - data = records_array.map do |record| - attrs = remove_pk(record.attributes) - attrs = attr_mapper.call(attrs) if attr_mapper - - attrs.each do |k, v| - attrs[k] = Sequel.hstore(v) if v.is_a?(Hash) - end - - attrs - end + data = persist_processor.process_insert(records) pks = DB[relation].returning(primary_key).multi_insert(data) pks.each_with_index.map do |pk, index| - set_key(records_array[index], pk) + set_key(records[index], pk) end end alias_method :create, :insert @@ -72,12 +68,10 @@ def insert(records) def update(record) records = Array(record) - attr_mapper = on_persist + result = persist_processor.process_update(records) - records.map do |entity| - params = remove_pk(entity.attributes) - params = attr_mapper.call(params) if attr_mapper - DB[relation].where(key_params(entity)).update(params) + result.data.each_with_index.map do |data, i| + DB[relation].where(result.pkeys[i]).update(data) end.reduce(&:+) end @@ -199,26 +193,6 @@ def relation(relation_name=nil) private - ## - # Private: deletes primary key if it not composite - # - # Params: - # - params {Hash} attributes of entity - # - # Examples: - # - # remove_pk({ id: 3, name: 'name' }) # => {name: 'name'} - # - # Returns: {Hash} cleared attributes of entity - # - def remove_pk(params) - if composite_key? - params - else - params.reject { |key| primary_key.include? key } - end - end - ## # Private: primary key columns name # diff --git a/spec/integration/dml/relation_dsl_spec.rb b/spec/integration/dml/relation_dsl_spec.rb index a6de501..41daa9d 100644 --- a/spec/integration/dml/relation_dsl_spec.rb +++ b/spec/integration/dml/relation_dsl_spec.rb @@ -52,7 +52,7 @@ end it 'should set foreign key' do - expect(subject.primary_key).to equal(:id) + expect(subject.primary_key).to contain_exactly(:id) end end diff --git a/spec/unit/dml/repository/persist_processor_spec.rb b/spec/unit/dml/repository/persist_processor_spec.rb new file mode 100644 index 0000000..76e2351 --- /dev/null +++ b/spec/unit/dml/repository/persist_processor_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Dml::Repository::PersistProcessor do + let(:instance) { described_class.new(relation_name, primary_keys, callbacks) } + + let(:relation_name) { :users } + let(:primary_keys) { [:id] } + let(:callbacks) { [] } + + let(:entities) do + [ + instance_double('Dml::Entity', attributes: { id: 1, company_id: 1, age: 18, name: 'John' }), + instance_double('Dml::Entity', attributes: { id: 2, company_id: 1, age: 21, name: 'Mary' }) + ] + end + + describe '#process_insert' do + subject { instance.process_insert(entities) } + + context 'with callbacks' do + let(:callbacks) do + [ + ->(attrs) { attrs[:age] = attrs[:age].to_s; attrs }, + ->(attrs) { attrs[:name] = attrs[:name].downcase; attrs } + ] + end + + it 'processes all entities' do + expect(subject.first).to include(age: '18', name: 'john') + expect(subject.last).to include(age: '21', name: 'mary') + end + end + + context 'without callbacks' do + let(:callbacks) { [] } + + it 'returns the same hash' do + expect(subject.first).to include(entities.first.attributes) + end + end + end + + describe '#process_update' do + subject { instance.process_update(entities) } + + context 'with callbacks' do + let(:callbacks) do + [ + ->(attrs) { attrs[:age] = attrs[:age].to_s; attrs }, + ->(attrs) { attrs[:name] = attrs[:name].downcase; attrs } + ] + end + + it 'processes all entities' do + expect(subject.data.first).to include(age: '18', name: 'john') + expect(subject.data.last).to include(age: '21', name: 'mary') + end + end + + context 'without callbacks' do + let(:callbacks) { [] } + + it 'returns the same hash' do + expect(subject.data.first).to include(entities.first.attributes) + end + end + + context 'with one pk' do + let(:primary_keys) { [:id] } + + it 'extracts pkeys' do + expect(subject.pkeys).to eql([{ id: 1}, { id: 2 }]) + end + + it 'does not touch anything' do + expect(subject.data.first).to include(entities.first.attributes) + expect(subject.data.last).to include(entities.last.attributes) + end + end + + context 'with many pks' do + let(:primary_keys) { [:id, :company_id] } + + it 'does not touch anything' do + expect(subject.data.first).to include(entities.first.attributes) + expect(subject.data.last).to include(entities.last.attributes) + end + end + + context 'when pk does not present in attributes' do + let(:primary_keys) { [:code] } + + it 'returns the same hash' do + expect(subject.data.first).to include(entities.first.attributes) + end + end + end + +end