Skip to content

Instantly share code, notes, and snippets.

@searls
Last active April 23, 2024 16:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save searls/ee2a2aab1d4bdbeeca3345991c1a8d97 to your computer and use it in GitHub Desktop.
Save searls/ee2a2aab1d4bdbeeca3345991c1a8d97 to your computer and use it in GitHub Desktop.
Is this overkill? Is there some other nice way to do this?
class ValidatesDeeply
Failure = Struct.new(:model, :message)
Result = Struct.new(:valid?, :failures)
def validate(model, visited = Set.new)
return Result.new(true) if visited.include?(model)
visited.add(model)
combine_results(Result.new(model.valid?, extract_failures(model)), *validate_associations(model, visited))
end
private
def combine_results(*results)
Result.new(results.all?(&:valid?), results.flat_map(&:failures).compact)
end
def extract_failures(model)
model.errors.full_messages.map { |message| Failure.new(model, message) }
end
SKIP_ASSOCIATED_TABLES = %w[
active_storage_attachments
active_storage_blobs
active_storage_variant_records
].freeze
def validate_associations(model, visited)
model.class.reflect_on_all_associations.reject { |assoc|
assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) ||
assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) ||
SKIP_ASSOCIATED_TABLES.include?(assoc.table_name)
}.flat_map do |association|
associated_records = model.send(association.name)
validate_association(associated_records, visited)
end
end
def validate_association(associated, visited)
case associated
when ActiveRecord::Base
validate(associated, visited)
when Enumerable
associated.map { |record| validate(record, visited) }
else
Result.new(true)
end
end
end
@zacharydanger
Copy link

Something certainly feels amiss here for sure.

Is there a reasonvalidates_associated doesn't fit? / Can you share anything about the concrete use-case?

@searls
Copy link
Author

searls commented Apr 23, 2024

I have a back-end CMS of a number of entities that take the admin a month to fully complete, so saving each layer of six related associations without serious validation of the associated records is very important as so much is WIP, so validates_associated would be onerous. However, as the scheduled publish date draws near, the system needs to surface all the ways in which the data is in good or bad shape.

To that ends, I built a (not pictured here) PORO that does a few dozen such health checks on the root model of interest, but as a last sanity check I also wanted to make sure that "if they were saved today" (potentially a month later, with any applicable code/migration changes that had occurred since creation), all of the associated records would still be considered valid, and—if not—surface that information in the health check too.

@zacharydanger
Copy link

So roughly: HighLevelThing -> lots of DependentAssociatedModels's, we don't necessarily care about Serious Validation™ until HighLevelModel flips some published? bit/enum.

There might still be a vanilla Rails approach:

#pseudo-rails code, obvs

class HighLevelThing
  has_many :whatevers
  validates_associated :whatevers, if: :published?
end

class Whatever
  belongs_to :high_level_thing

  # serious validation saved for high level publishing
  validates :whatever, if: -> { high_level_thing.published? }
end

Definitely a matter of taste—I'm sure ValidatesDeeply does the thing you want it to, but 6+ months out I know which code I'd rather have forgotten. 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment