Skip to content

Commit

Permalink
Merge pull request #86 from rom-rb/support-many-to-many
Browse files Browse the repository at this point in the history
Introduce ManyToMany and OneToOneThrough builder
  • Loading branch information
solnic authored Jan 19, 2024
2 parents fb42523 + 5be2dc2 commit 38440e8
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 92 deletions.
4 changes: 4 additions & 0 deletions lib/rom/factory/attribute_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def associations
self.class.new(elements.select { |e| e.is_a?(Attributes::Association::Core) })
end

def reject(&block)
self.class.new(elements.reject(&block))
end

private

# @api private
Expand Down
124 changes: 91 additions & 33 deletions lib/rom/factory/attributes/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module ROM::Factory
module Attributes
# @api private
# rubocop:disable Style/OptionalArguments
module Association
class << self
def new(assoc, builder, *traits, **options)
Expand Down Expand Up @@ -46,28 +47,62 @@ def dependency?(*)
def value?
false
end

# @api private
def factories
builder.factories
end

# @api private
def foreign_key
assoc.foreign_key
end

# @api private
def count
options.fetch(:count, 1)
end
end

# @api private
class ManyToOne < Core
# @api private
# rubocop:disable Metrics/AbcSize
def call(attrs, persist: true)
if attrs.key?(name) && !attrs[foreign_key]
return if attrs.key?(name) && attrs[name].nil?

assoc_data = attrs.fetch(name, EMPTY_HASH)

if assoc_data.is_a?(Hash) && assoc_data[assoc.target.primary_key] && !attrs[foreign_key]
assoc.associate(attrs, attrs[name])
elsif !attrs[foreign_key]
struct = if persist
builder.persistable.create(*traits)
elsif assoc_data.is_a?(ROM::Struct)
assoc.associate(attrs, assoc_data)
else
parent = if persist && !attrs[foreign_key]
builder.persistable.create(*parent_traits, **assoc_data)
else
builder.struct(*traits)
builder.struct(
*parent_traits,
**assoc_data.merge(assoc.target.primary_key => attrs[foreign_key])
)
end
tuple = {name => struct}
assoc.associate(tuple, struct)

tuple = {name => parent}

assoc.associate(tuple, parent)
end
end
# rubocop:enable Metrics/AbcSize

# @api private
def foreign_key
assoc.foreign_key
private

def parent_traits
@parent_traits ||=
if assoc.target.associations.key?(assoc.source.name)
traits + [assoc.target.associations[assoc.source.name].key => false]
else
traits
end
end
end

Expand Down Expand Up @@ -95,11 +130,6 @@ def call(attrs = EMPTY_HASH, parent, persist: true)
def dependency?(rel)
assoc.source == rel
end

# @api private
def count
options.fetch(:count)
end
end

# @api private
Expand All @@ -124,28 +154,45 @@ def call(attrs = EMPTY_HASH, parent, persist: true)

{name => struct}
end

# @api private
def count
options.fetch(:count, 1)
end
end

class OneToOneThrough < Core
class ManyToMany < Core
def call(attrs = EMPTY_HASH, parent, persist: true)
return if attrs.key?(name)

struct = if persist && attrs[tpk]
attrs
elsif persist
builder.persistable.create(*traits, **attrs)
else
builder.struct(*traits, **attrs)
end
structs = count.times.map do
if persist && attrs[tpk]
attrs
elsif persist
builder.persistable.create(*traits, **attrs)
else
builder.struct(*traits, **attrs)
end
end

assoc.persist([parent], struct) if persist
# Delegate to through factory if it exists
if persist
if through_factory?
structs.each do |child|
through_attrs = {
Dry::Core::Inflector.singularize(assoc.source.name.key).to_sym => parent,
assoc.through.assoc_name => child
}

factories[through_factory_name, **through_attrs]
end
else
assoc.persist([parent], structs)
end

{name => struct}
{name => result(structs)}
else
result(structs)
end
end

def result(structs)
{name => structs}
end

def dependency?(rel)
Expand All @@ -156,16 +203,27 @@ def through?
true
end

private
def through_factory?
factories.registry.key?(through_factory_name)
end

def count
options.fetch(:count, 1)
def through_factory_name
ROM::Inflector.singularize(assoc.definition.through.source).to_sym
end

private

def tpk
assoc.target.primary_key
end
end

class OneToOneThrough < ManyToMany
def result(structs)
{name => structs[0]}
end
end
end
end
# rubocop:enable Style/OptionalArguments
end
10 changes: 9 additions & 1 deletion lib/rom/factory/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class Builder
# @return [Module] Custom struct namespace
option :struct_namespace, reader: false

# @!attribute [r] factories
# @return [Module] Factories with other builders
option :factories, reader: true, optional: true

# @api private
def tuple(*traits, **attrs)
tuple_evaluator.defaults(traits, attrs)
Expand Down Expand Up @@ -57,7 +61,11 @@ def persistable

# @api private
def tuple_evaluator
@__tuple_evaluator__ ||= TupleEvaluator.new(attributes, tuple_evaluator_relation, traits)
@__tuple_evaluator__ ||= TupleEvaluator.new(
attributes,
tuple_evaluator_relation,
traits
)
end

# @api private
Expand Down
3 changes: 2 additions & 1 deletion lib/rom/factory/builder/persistable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ def initialize(builder, relation = builder.relation)

# @api private
def create(*traits, **attrs)
tuple = tuple(*traits, **attrs)
validate_keys(traits, attrs)

tuple = tuple(*traits, **attrs)
persisted = persist(tuple)

if tuple_evaluator.has_associations?(traits)
Expand Down
8 changes: 7 additions & 1 deletion lib/rom/factory/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ def initialize(name, relation:, factories:, struct_namespace:, attributes: Attri

# @api private
def call
::ROM::Factory::Builder.new(_attributes, _traits, relation: _relation, struct_namespace: _struct_namespace)
::ROM::Factory::Builder.new(
_attributes,
_traits,
relation: _relation,
struct_namespace: _struct_namespace,
factories: _factories
)
end

# Delegate to a builder and persist a struct
Expand Down
Loading

0 comments on commit 38440e8

Please sign in to comment.