still refactoring the associations + add a default order_by for the has_many association in content entries
did
committed Mar 04, 2015
commit c61fb7abb99ff1b134022ada0ee1bad15f124f13
Showing 12
changed files with
192 additions
and 148 deletions
locomotive/steam/entities/content_entry.rb b/lib/locomotive/steam/entities/content_entry.rb
+0
-11
| @@ | @@ -4,8 +4,6 @@ module Locomotive::Steam |
| class ContentEntry | |
| - | # ASSOCIATION_NAMES = [:belongs_to, :has_many, :many_to_many].freeze |
| - | |
| include Locomotive::Steam::Models::Entity | |
| attr_accessor :content_type | |
| @@ | @@ -77,8 +75,6 @@ module Locomotive::Steam |
| end | |
| def _cast_value(field) | |
| - | # if ASSOCIATION_NAMES.include?(field.type) |
| - | # AssociationMetadata.new(field.type, self, field, [*attributes[field.name]]) |
| if private_methods.include?(:"_cast_#{field.type}") | |
| send(:"_cast_#{field.type}", field.name) | |
| else | |
| @@ | @@ -123,13 +119,6 @@ module Locomotive::Steam |
| end | |
| end | |
| - | # class AssociationMetadata < Struct.new(:type, :source, :field, :target_slugs) |
| - | # def association; true; end |
| - | # def inverse_of; field.inverse_of; end |
| - | # def target_class_slug; field.class_name; end |
| - | # def order_by; field[:order_by]; end |
| - | # end |
| - | |
| end | |
| end | |
locomotive/steam/entities/content_type.rb b/lib/locomotive/steam/entities/content_type.rb
+1
-1
| @@ | @@ -5,7 +5,7 @@ module Locomotive::Steam |
| include Locomotive::Steam::Models::Entity | |
| extend Forwardable | |
| - | def_delegators :fields, :localized_fields_names, :belongs_to_fields, :has_many_fields |
| + | def_delegators :fields, :localized_fields_names, :association_fields |
| def initialize(attributes = {}) | |
| super({ | |
locomotive/steam/entities/content_type_field.rb b/lib/locomotive/steam/entities/content_type_field.rb
+11
-1
| @@ | @@ -23,6 +23,12 @@ module Locomotive::Steam |
| self[:class_name] || self[:target] | |
| end | |
| + | def order_by |
| + | return self[:order_by] if self[:order_by] |
| + | |
| + | type == :has_many ? "position_in_#{self[:inverse_of]}" : nil |
| + | end |
| + | |
| alias :target :class_name | |
| def target_id | |
| @@ | @@ -39,7 +45,11 @@ module Locomotive::Steam |
| def localized?; self[:localized]; end | |
| def association_options | |
| - | @attributes.slice(:inverse_of, :order_by).merge(class_name: class_name) |
| + | { |
| + | target_id: target_id, |
| + | inverse_of: self[:inverse_of], |
| + | order_by: order_by |
| + | } |
| end | |
| class SelectOption | |
locomotive/steam/models/associations/has_many.rb b/lib/locomotive/steam/models/associations/has_many.rb
+4
-2
| @@ | @@ -4,7 +4,6 @@ module Locomotive::Steam |
| # Note: represents an embedded collection | |
| class HasManyAssociation < ReferencedAssociation | |
| - | # TODO: use order_by from options (if specified, "position_in_<@name>" by default |
| def __load__ | |
| # Note: in adapters like the FileSystem one, we use slugs | |
| # to reference other entities in associations. | |
| @@ | @@ -14,7 +13,10 @@ module Locomotive::Steam |
| # all the further queries will be scoped by the "foreign_key" | |
| @repository.local_conditions[key] = id | |
| - | # all the further methods will be delegated to @repository |
| + | # use order_by from options as the default one for further queries |
| + | @repository.local_conditions[:order_by] = @options[:order_by] unless @options[:order_by].blank? |
| + | |
| + | # all the further calls (method_missing) will be delegated to @repository |
| @repository | |
| end | |
locomotive/steam/models/associations/referenced.rb b/lib/locomotive/steam/models/associations/referenced.rb
+10
-1
| @@ | @@ -27,8 +27,17 @@ module Locomotive::Steam |
| # needs implementation | |
| end | |
| + | def __call_block_once__ |
| + | # setup the repository if custom configuration from the |
| + | # repository for instance. |
| + | if @block |
| + | @block.call(@repository, @options) |
| + | @block = nil # trick to call it only once |
| + | end |
| + | end |
| + | |
| def method_missing(name, *args, &block) | |
| - | @block.call(@repository) if @block |
| + | __call_block_once__ |
| __load__.try(:send, name, *args, &block) | |
| end | |
locomotive/steam/models/mapper.rb b/lib/locomotive/steam/models/mapper.rb
+6
-2
| @@ | @@ -30,11 +30,15 @@ module Locomotive::Steam |
| end | |
| ASSOCIATION_CLASSES.each do |type, _| | |
| - | define_method("#{type}_association") do |name, repository_klass, options = {}, &block| |
| - | @associations << [type, name.to_sym, repository_klass, options || {}, block] |
| + | define_method("#{type}_association") do |name, repository_klass, options = nil, &block| |
| + | association(type, name, repository_klass, options, &block) |
| end | |
| end | |
| + | def association(type, name, repository_klass, options = nil, &block) |
| + | @associations << [type, name.to_sym, repository_klass, options || {}, block] |
| + | end |
| + | |
| def to_entity(attributes) | |
| entity_klass.new(deserialize(attributes)).tap do |entity| | |
| attach_entity_to_associations(entity) | |
locomotive/steam/models/repository.rb b/lib/locomotive/steam/models/repository.rb
+11
-1
| @@ | @@ -8,13 +8,14 @@ module Locomotive::Steam |
| class RecordNotFound < StandardError; end | |
| - | attr_accessor :adapter, :scope |
| + | attr_accessor :adapter, :scope, :local_conditions |
| def_delegators :@scope, :site, :site=, :locale, :locale= | |
| def initialize(adapter, site = nil, locale = nil) | |
| @adapter = adapter | |
| @scope = Scope.new(site, locale) | |
| + | @local_conditions = {} |
| end | |
| def build(attributes, &block) | |
| @@ | @@ -63,6 +64,15 @@ module Locomotive::Steam |
| mapper.i18n_value_of(entity, name, locale) | |
| end | |
| + | def prepare_conditions(*conditions) |
| + | first = { order_by: @local_conditions.delete(:order_by) }.delete_if { |_, v| v.blank? } |
| + | |
| + | [first, *conditions].inject({}) do |memo, hash| |
| + | memo.merge!(hash) unless hash.blank? |
| + | memo |
| + | end.merge(@local_conditions) |
| + | end |
| + | |
| # TODO: not sure about that. could it be used further in the dev | |
| # def collection_name | |
| # mapper.name | |
locomotive/steam/repositories/content_entry_repository.rb b/lib/locomotive/steam/repositories/content_entry_repository.rb
+24
-58
| @@ | @@ -5,13 +5,13 @@ module Locomotive |
| include Models::Repository | |
| - | attr_accessor :content_type_repository, :content_type, :local_conditions |
| + | attr_accessor :content_type_repository, :content_type |
| def initialize(adapter, site = nil, locale = nil, content_type_repository = nil) | |
| - | @local_conditions = {} |
| @adapter = adapter | |
| @scope = Locomotive::Steam::Models::Scope.new(site, locale) | |
| @content_type_repository = content_type_repository | |
| + | @local_conditions = {} |
| end | |
| # Entity mapping | |
| @@ | @@ -21,7 +21,7 @@ module Locomotive |
| default_attribute :content_type, -> (repository) { repository.content_type } | |
| end | |
| - | # this is the starting point of all the next actions |
| + | # this is the starting point of all the next methods |
| def with(type) | |
| self.content_type = type # used for creating the scope | |
| self.scope.context[:content_type] = type | |
| @@ | @@ -88,8 +88,7 @@ module Locomotive |
| def mapper(memoized = false) | |
| super(memoized).tap do |mapper| | |
| add_localized_fields_to_mapper(mapper) | |
| - | add_belongs_to_fields_to_mapper(mapper) |
| - | add_has_many_fields_to_mapper(mapper) |
| + | add_associations_to_mapper(mapper) |
| end | |
| end | |
| @@ | @@ -99,57 +98,37 @@ module Locomotive |
| end | |
| end | |
| - | def add_belongs_to_fields_to_mapper(mapper) |
| - | self.content_type.belongs_to_fields.each do |field| |
| - | mapper.belongs_to_association(field.name, self.class) do |repository| |
| - | # Note: this code will be executed only when the attribute will be called |
| + | def add_associations_to_mapper(mapper) |
| + | self.content_type.association_fields.each do |field| |
| + | mapper.association(field.type, field.name, self.class, field.association_options, &method(:prepare_repository_for_association)) |
| + | end |
| + | end |
| - | # load the target content type |
| - | _content_type = content_type_repository.find(field.target_id) |
| + | # This code is executed once when the association proxy object receives a call to any method |
| + | def prepare_repository_for_association(repository, options) |
| + | # load the target content type |
| + | _content_type = content_type_repository.find(options[:target_id]) |
| - | # the target repository uses this content type for all the other inner calls |
| - | repository.with(_content_type) |
| + | # the target repository uses this content type for all the other inner calls |
| + | repository.with(_content_type) |
| - | # the content type repository is also need by the target repository |
| - | repository.content_type_repository = content_type_repository |
| - | end |
| - | end |
| + | # the content type repository is also need by the target repository |
| + | repository.content_type_repository = content_type_repository |
| end | |
| - | def add_has_many_fields_to_mapper(mapper) |
| - | self.content_type.has_many_fields.each do |field| |
| - | mapper.has_many_association(field.name, self.class, inverse_of: field.inverse_of) do |repository| |
| - | # Note: this code will be executed only when the attribute will be called |
| + | def next_or_previous(entry, asc_op, desc_op) |
| + | return nil if entry.nil? |
| - | # load the target content type |
| - | _content_type = content_type_repository.find(field.target_id) |
| + | with(entry.content_type) |
| - | # the target repository uses this content type for all the other inner calls |
| - | repository.with(_content_type) |
| + | name, direction = self.content_type.order_by.split |
| + | op = direction == 'asc' ? asc_op : desc_op |
| - | # the content type repository is also need by the target repository |
| - | repository.content_type_repository = content_type_repository |
| - | end |
| - | end |
| - | end |
| + | conditions = prepare_conditions({ k(name, op) => i18n_value_of(entry, name) }) |
| - | # TODO: move to the repository + handle order_by |
| - | def prepare_conditions(*conditions) |
| - | [*conditions].inject({}) do |memo, hash| |
| - | memo.merge!(hash) unless hash.blank? |
| - | memo |
| - | end.merge(@local_conditions) |
| + | first { where(conditions) } |
| end | |
| - | # def type_from(slug) |
| - | # content_type_repository.by_slug(slug) |
| - | # end |
| - | |
| - | # def localized_slug(entry) |
| - | # raise 'SHOULD NOT BE USED' |
| - | # localized_attribute(entry, :_slug) |
| - | # end |
| - | |
| # def association(metadata, conditions = {}) | |
| # case metadata.type | |
| # when :belongs_to then belongs_to_association(metadata) | |
| @@ | @@ -183,19 +162,6 @@ module Locomotive |
| # all(type, conditions) | |
| # end | |
| - | def next_or_previous(entry, asc_op, desc_op) |
| - | return nil if entry.nil? |
| - | |
| - | with(entry.content_type) |
| - | |
| - | name, direction = self.content_type.order_by.split |
| - | op = direction == 'asc' ? asc_op : desc_op |
| - | |
| - | conditions = prepare_conditions({ k(name, op) => i18n_value_of(entry, name) }) |
| - | |
| - | first { where(conditions) } |
| - | end |
| - | |
| end | |
| end | |
locomotive/steam/repositories/content_type_field_repository.rb b/lib/locomotive/steam/repositories/content_type_field_repository.rb
+10
-6
| @@ | @@ -14,6 +14,10 @@ module Locomotive |
| embedded_association :select_options, ContentTypeFieldSelectOptionRepository | |
| end | |
| + | def associations |
| + | query { where(k(:type, :in) => %i(belongs_to has_many many_to_many)) }.all |
| + | end |
| + | |
| def unique | |
| query { where(unique: true) }.all.inject({}) do |memo, field| | |
| memo[field.name] = field | |
| @@ | @@ -25,13 +29,13 @@ module Locomotive |
| query { where(required: true) }.all | |
| end | |
| - | def belongs_to |
| - | query { where(type: :belongs_to) }.all |
| - | end |
| + | # def belongs_to |
| + | # query { where(type: :belongs_to) }.all |
| + | # end |
| - | def has_many |
| - | query { where(type: :has_many) }.all |
| - | end |
| + | # def has_many |
| + | # query { where(type: :has_many) }.all |
| + | # end |
| def localized_names | |
| query { where(localized: true) }.all.map(&:name) | |
spec/spec_helper.rb
+0
-2
| @@ | @@ -27,8 +27,6 @@ require 'bundler/setup' |
| require 'i18n-spec' | |
| require_relative '../lib/locomotive/steam' | |
| - | # TODO |
| - | # require_relative '../lib/locomotive/steam/repositories/filesystem' |
| require_relative 'support' | |
| Locomotive::Steam.configure do |config| | |
spec/unit/entities/content_type_field_spec.rb
+54
-1
| @@ | @@ -2,7 +2,8 @@ require 'spec_helper' |
| describe Locomotive::Steam::ContentTypeField do | |
| - | let(:content_type) { described_class.new(name: 'title', type: 'string') } |
| + | let(:attributes) { { name: 'title', type: 'string' } } |
| + | let(:content_type) { described_class.new(attributes) } |
| describe '#type' do | |
| @@ | @@ -11,4 +12,56 @@ describe Locomotive::Steam::ContentTypeField do |
| end | |
| + | describe '#order_by' do |
| + | |
| + | subject { content_type.order_by } |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'has_many field' do |
| + | |
| + | let(:attributes) { { name: 'articles', type: 'has_many', inverse_of: 'author' } } |
| + | it { is_expected.to eq 'position_in_author' } |
| + | |
| + | context 'order_by is specified' do |
| + | |
| + | let(:attributes) { { name: 'articles', type: 'has_many', inverse_of: 'author', order_by: 'name asc' } } |
| + | it { is_expected.to eq 'name asc' } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | describe '#target_id' do |
| + | |
| + | subject { content_type.target_id } |
| + | it { is_expected.to eq nil } |
| + | |
| + | context 'slug' do |
| + | |
| + | let(:attributes) { { name: 'articles', class_name: 'articles' } } |
| + | it { is_expected.to eq 'articles' } |
| + | |
| + | end |
| + | |
| + | context 'class name' do |
| + | |
| + | let(:attributes) { { name: 'articles', class_name: 'Locomotive::ContentEntry42' } } |
| + | it { is_expected.to eq '42' } |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | describe '#association_options' do |
| + | |
| + | let(:attributes) { { name: 'articles', class_name: 'articles', type: 'has_many', inverse_of: 'author' } } |
| + | |
| + | subject { content_type.association_options } |
| + | |
| + | it { is_expected.to eq({ target_id: 'articles', inverse_of: 'author', order_by: 'position_in_author' }) } |
| + | |
| + | end |
| + | |
| end | |
spec/unit/repositories/content_entry_repository_spec.rb
+61
-62
| @@ | @@ -18,66 +18,6 @@ describe Locomotive::Steam::ContentEntryRepository do |
| adapter.cache = NoCacheStore.new | |
| end | |
| - | describe 'belongs_to' do |
| - | |
| - | let(:field) { instance_double('Field', name: :author, type: :belongs_to, target_id: 2) } |
| - | let(:type) { build_content_type('Articles', label_field_name: :title, belongs_to_fields: [field]) } |
| - | let(:entries) { [{ content_type_id: 1, title: 'Hello world', author_id: 'john-doe' }] } |
| - | let(:other_type) { build_content_type('Authors', _id: 2, label_field_name: :name, fields_by_name: { name: instance_double('Field', name: :name, type: :string) }) } |
| - | let(:other_entries) { [{ content_type_id: 2, _slug: 'john-doe', name: 'John Doe' }] } |
| - | |
| - | let(:type_repository) { instance_double('ContentTypeRepository', belongs_to: [field]) } |
| - | |
| - | before do |
| - | allow(type).to receive(:fields).and_return(type_repository) |
| - | allow(content_type_repository).to receive(:find).with(2).and_return(other_type) |
| - | end |
| - | |
| - | subject { repository.with(type).by_slug('hello-world') } |
| - | |
| - | it { expect(subject.author.class).to eq Locomotive::Steam::Models::BelongsToAssociation } |
| - | |
| - | it 'calls the new repository to fetch the target entity' do |
| - | author = subject.author |
| - | allow(adapter).to receive(:collection).and_return(other_entries) |
| - | expect(author.name).to eq 'John Doe' |
| - | end |
| - | |
| - | end |
| - | |
| - | describe 'has_many' do |
| - | |
| - | let(:field) { instance_double('Field', name: :articles, type: :has_many, target_id: 2, inverse_of: :author) } |
| - | let(:type) { build_content_type('Authors', label_field_name: :name, has_many_fields: [field]) } |
| - | let(:entries) { [{ content_type_id: 1, _id: 1, name: 'John Doe' }] } |
| - | let(:other_type) { build_content_type('Articles', _id: 2, label_field_name: :title, fields_by_name: { name: instance_double('Field', name: :title, type: :string) }) } |
| - | let(:other_entries) { |
| - | [ |
| - | { content_type_id: 2, _slug: 'hello-world', title: 'Hello world', author_id: 'john-doe' }, |
| - | { content_type_id: 2, _slug: 'lorem-ipsum', title: 'Lorem ipsum', author_id: 'john-doe' }, |
| - | { content_type_id: 2, _slug: 'lost', title: 'Lost', author_id: 'jane-doe' }, |
| - | ] |
| - | } |
| - | |
| - | let(:type_repository) { instance_double('ContentTypeRepository', has_many: [field]) } |
| - | |
| - | before do |
| - | allow(type).to receive(:fields).and_return(type_repository) |
| - | allow(content_type_repository).to receive(:find).with(2).and_return(other_type) |
| - | end |
| - | |
| - | subject { repository.with(type).by_slug('john-doe') } |
| - | |
| - | it { expect(subject.articles.class).to eq Locomotive::Steam::Models::HasManyAssociation } |
| - | |
| - | it 'calls the new repository to fetch the target entities' do |
| - | articles = subject.articles |
| - | allow(adapter).to receive(:collection).and_return(other_entries) |
| - | expect(articles.all.map(&:title)).to eq ['Hello world', 'Lorem ipsum'] |
| - | end |
| - | |
| - | end |
| - | |
| describe '#all' do | |
| let(:conditions) { nil } | |
| @@ | @@ -250,6 +190,66 @@ describe Locomotive::Steam::ContentEntryRepository do |
| end | |
| + | describe 'belongs_to' do |
| + | |
| + | let(:field) { instance_double('Field', name: :author, type: :belongs_to, association_options: { target_id: 2 }) } |
| + | let(:type) { build_content_type('Articles', label_field_name: :title, association_fields: [field]) } |
| + | let(:entries) { [{ content_type_id: 1, title: 'Hello world', author_id: 'john-doe' }] } |
| + | let(:other_type) { build_content_type('Authors', _id: 2, label_field_name: :name, fields_by_name: { name: instance_double('Field', name: :name, type: :string) }) } |
| + | let(:other_entries) { [{ content_type_id: 2, _slug: 'john-doe', name: 'John Doe' }] } |
| + | |
| + | let(:type_repository) { instance_double('ContentTypeRepository', belongs_to: [field]) } |
| + | |
| + | before do |
| + | allow(type).to receive(:fields).and_return(type_repository) |
| + | allow(content_type_repository).to receive(:find).with(2).and_return(other_type) |
| + | end |
| + | |
| + | subject { repository.with(type).by_slug('hello-world') } |
| + | |
| + | it { expect(subject.author.class).to eq Locomotive::Steam::Models::BelongsToAssociation } |
| + | |
| + | it 'calls the new repository to fetch the target entity' do |
| + | author = subject.author |
| + | allow(adapter).to receive(:collection).and_return(other_entries) |
| + | expect(author.name).to eq 'John Doe' |
| + | end |
| + | |
| + | end |
| + | |
| + | describe 'has_many' do |
| + | |
| + | let(:field) { instance_double('Field', name: :articles, type: :has_many, association_options: { target_id: 2, inverse_of: :author, order_by: 'position_in_author' }) } |
| + | let(:type) { build_content_type('Authors', label_field_name: :name, association_fields: [field]) } |
| + | let(:entries) { [{ content_type_id: 1, _id: 1, name: 'John Doe' }] } |
| + | let(:other_type) { build_content_type('Articles', _id: 2, label_field_name: :title, fields_by_name: { name: instance_double('Field', name: :title, type: :string) }) } |
| + | let(:other_entries) { |
| + | [ |
| + | { content_type_id: 2, _slug: 'hello-world', title: 'Hello world', author_id: 'john-doe', position_in_author: 2 }, |
| + | { content_type_id: 2, _slug: 'lorem-ipsum', title: 'Lorem ipsum', author_id: 'john-doe', position_in_author: 1 }, |
| + | { content_type_id: 2, _slug: 'lost', title: 'Lost', author_id: 'jane-doe' }, |
| + | ] |
| + | } |
| + | |
| + | let(:type_repository) { instance_double('ContentTypeRepository', has_many: [field]) } |
| + | |
| + | before do |
| + | allow(type).to receive(:fields).and_return(type_repository) |
| + | allow(content_type_repository).to receive(:find).with(2).and_return(other_type) |
| + | end |
| + | |
| + | subject { repository.with(type).by_slug('john-doe') } |
| + | |
| + | it { expect(subject.articles.class).to eq Locomotive::Steam::Models::HasManyAssociation } |
| + | |
| + | it 'calls the new repository to fetch the target entities' do |
| + | articles = subject.articles |
| + | allow(adapter).to receive(:collection).and_return(other_entries) |
| + | expect(articles.all.map(&:title)).to eq ['Lorem ipsum', 'Hello world'] |
| + | end |
| + | |
| + | end |
| + | |
| def build_content_type(name, attributes = {}) | |
| instance_double(name, | |
| { | |
| @@ | @@ -257,8 +257,7 @@ describe Locomotive::Steam::ContentEntryRepository do |
| slug: name.to_s.downcase, | |
| order_by: nil, | |
| localized_fields_names: [], | |
| - | belongs_to_fields: [], |
| - | has_many_fields: [], |
| + | association_fields: [], |
| fields_by_name: {} | |
| }.merge(attributes)) | |
| end | |