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