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

Introduce ManyToMany and OneToOneThrough builder #86

Merged
merged 13 commits into from
Jan 19, 2024
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
Loading