Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for the Many-to-One association #398

Merged
merged 8 commits into from May 22, 2017
85 changes: 83 additions & 2 deletions lib/hanami/model/associations/belongs_to.rb
Expand Up @@ -5,14 +5,95 @@ module Model
module Associations
# Many-To-One association
#
# @since 0.7.0
# @since x.x.x
# @api private
class BelongsTo
# @since 0.7.0
# @since x.x.x
# @api private
def self.schema_type(entity)
Sql::Types::Schema::AssociationType.new(entity)
end

# @since x.x.x
# @api private
attr_reader :repository

# @since x.x.x
# @api private
attr_reader :source

# @since x.x.x
# @api private
attr_reader :target

# @since x.x.x
# @api private
attr_reader :subject

# @since x.x.x
# @api private
attr_reader :scope

# @since x.x.x
# @api private
def initialize(repository, source, target, subject, scope = nil)
@repository = repository
@source = source
@target = target
@subject = subject.to_hash unless subject.nil?
@scope = scope || _build_scope
freeze
end

# @since x.x.x
# @api private
def one
scope.one
end

private

# @since x.x.x
# @api private
def container
repository.container
end

# @since x.x.x
# @api private
def primary_key
association_keys.first
end

# @since x.x.x
# @api private
def relation(name)
repository.relations[Hanami::Utils::String.new(name).pluralize]
end

# @since x.x.x
# @api private
def foreign_key
association_keys.last
end

# Returns primary key and foreign key
#
# @since x.x.x
# @api private
def association_keys
relation(source)
.associations[target]
.__send__(:join_key_map, container.relations)
end

# @since x.x.x
# @api private
def _build_scope
result = relation(target)
result = result.where(foreign_key => subject.fetch(primary_key)) unless subject.nil?
result.as(Model::MappedRelation.mapper_name)
end
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/hanami/model/associations/dsl.rb
Expand Up @@ -19,9 +19,10 @@ def has_many(relation, *)
@repository.__send__(:relations, relation)
end

# @since 0.7.0
# @since x.x.x
# @api private
def belongs_to(*)
def belongs_to(relation, *)
@repository.__send__(:relations, Hanami::Utils::String.new(relation).pluralize.to_sym)
end
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/hanami/model/error.rb
Expand Up @@ -60,7 +60,7 @@ def initialize(message = 'Constraint has been violated')
# Error for Unique Constraint Violation
#
# @since 0.6.1
class UniqueConstraintViolationError < Error
class UniqueConstraintViolationError < ConstraintViolationError
# @since 0.6.1
# @api private
def initialize(message = 'Unique constraint has been violated')
Expand All @@ -71,7 +71,7 @@ def initialize(message = 'Unique constraint has been violated')
# Error for Foreign Key Constraint Violation
#
# @since 0.6.1
class ForeignKeyConstraintViolationError < Error
class ForeignKeyConstraintViolationError < ConstraintViolationError
# @since 0.6.1
# @api private
def initialize(message = 'Foreign key constraint has been violated')
Expand All @@ -82,7 +82,7 @@ def initialize(message = 'Foreign key constraint has been violated')
# Error for Not Null Constraint Violation
#
# @since 0.6.1
class NotNullConstraintViolationError < Error
class NotNullConstraintViolationError < ConstraintViolationError
# @since 0.6.1
# @api private
def initialize(message = 'NOT NULL constraint has been violated')
Expand All @@ -93,7 +93,7 @@ def initialize(message = 'NOT NULL constraint has been violated')
# Error for Check Constraint Violation raised by Sequel
#
# @since 0.6.1
class CheckConstraintViolationError < Error
class CheckConstraintViolationError < ConstraintViolationError
# @since 0.6.1
# @api private
def initialize(message = 'Check constraint has been violated')
Expand Down
28 changes: 28 additions & 0 deletions spec/integration/associations/belongs_to_spec.rb
@@ -0,0 +1,28 @@
RSpec.describe 'Associations (belongs_To)' do
it "returns nil if association wasn't preloaded" do
repository = BookRepository.new
book = repository.create(name: 'L')
found = repository.find(book.id)

expect(found.author).to be(nil)
end

it 'preloads the associated record' do
repository = BookRepository.new
author = AuthorRepository.new.create(name: 'Michel Foucault')
book = repository.create(author_id: author.id, title: 'Surveiller et punir')
found = repository.find_with_author(book.id)

expect(found).to eq(book)
expect(found.author).to eq(author)
end

it 'returns an author' do
repository = BookRepository.new
author = AuthorRepository.new.create(name: 'Maurice Leblanc')
book = repository.create(author_id: author.id, title: "L'Aguille Creuse")
found = repository.author_for(book)

expect(found).to eq(author)
end
end
8 changes: 8 additions & 0 deletions spec/support/fixtures.rb
Expand Up @@ -138,6 +138,14 @@ class BookRepository < Hanami::Repository
associations do
belongs_to :author
end

def find_with_author(id)
aggregate(:author).where(id: id).as(Book).one
end

def author_for(book)
assoc(:author, book).one
end
end

class OperatorRepository < Hanami::Repository
Expand Down
13 changes: 13 additions & 0 deletions spec/unit/hanami/model/check_constraint_validation_error_spec.rb
@@ -0,0 +1,13 @@
RSpec.describe Hanami::Model::CheckConstraintViolationError do
it "inherits from Hanami::Model::ConstraintViolationError" do
expect(described_class.ancestors).to include(Hanami::Model::ConstraintViolationError)
end

it "has a default error message" do
expect { raise described_class }.to raise_error(described_class, "Check constraint has been violated")
end

it "allows custom error message" do
expect { raise described_class.new("Ouch") }.to raise_error(described_class, "Ouch")
end
end
13 changes: 13 additions & 0 deletions spec/unit/hanami/model/constraint_violation_error_spec.rb
@@ -0,0 +1,13 @@
RSpec.describe Hanami::Model::ConstraintViolationError do
it "inherits from Hanami::Model::Error" do
expect(described_class.ancestors).to include(Hanami::Model::Error)
end

it "has a default error message" do
expect { raise described_class }.to raise_error(described_class, "Constraint has been violated")
end

it "allows custom error message" do
expect { raise described_class.new("Ouch") }.to raise_error(described_class, "Ouch")
end
end
5 changes: 5 additions & 0 deletions spec/unit/hanami/model/error_spec.rb
@@ -0,0 +1,5 @@
RSpec.describe Hanami::Model::Error do
it "inherits from StandardError" do
expect(described_class.ancestors).to include(StandardError)
end
end
@@ -0,0 +1,13 @@
RSpec.describe Hanami::Model::ForeignKeyConstraintViolationError do
it "inherits from Hanami::Model::ConstraintViolationError" do
expect(described_class.ancestors).to include(Hanami::Model::ConstraintViolationError)
end

it "has a default error message" do
expect { raise described_class }.to raise_error(described_class, "Foreign key constraint has been violated")
end

it "allows custom error message" do
expect { raise described_class.new("Ouch") }.to raise_error(described_class, "Ouch")
end
end
13 changes: 13 additions & 0 deletions spec/unit/hanami/model/not_null_constraint_violation_error_spec.rb
@@ -0,0 +1,13 @@
RSpec.describe Hanami::Model::NotNullConstraintViolationError do
it "inherits from Hanami::Model::ConstraintViolationError" do
expect(described_class.ancestors).to include(Hanami::Model::ConstraintViolationError)
end

it "has a default error message" do
expect { raise described_class }.to raise_error(described_class, "NOT NULL constraint has been violated")
end

it "allows custom error message" do
expect { raise described_class.new("Ouch") }.to raise_error(described_class, "Ouch")
end
end
13 changes: 13 additions & 0 deletions spec/unit/hanami/model/unique_constraint_violation_error_spec.rb
@@ -0,0 +1,13 @@
RSpec.describe Hanami::Model::UniqueConstraintViolationError do
it "inherits from Hanami::Model::ConstraintViolationError" do
expect(described_class.ancestors).to include(Hanami::Model::ConstraintViolationError)
end

it "has a default error message" do
expect { raise described_class }.to raise_error(described_class, "Unique constraint has been violated")
end

it "allows custom error message" do
expect { raise described_class.new("Ouch") }.to raise_error(described_class, "Ouch")
end
end