Skip to content

Instantly share code, notes, and snippets.

@jfeaver
Last active September 30, 2022 13:21
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 jfeaver/d5de672f8578a13385bf4b9a251384b6 to your computer and use it in GitHub Desktop.
Save jfeaver/d5de672f8578a13385bf4b9a251384b6 to your computer and use it in GitHub Desktop.
Use Dry gems to build a struct which has an active record relation from a specified model as an attribute
require 'dry-struct'
require 'active_record'
module Types
include Dry.Types
def self.ArRelation(model_class)
# Note that ActiveRecord_Relation, ActiveRecord_Associations_CollectionProxy, and ActiveRecord_AssociationRelation
# are private constants - they may change some day
Instance(model_class.const_get(:ActiveRecord_Relation)) |
Instance(model_class.const_get(:ActiveRecord_Associations_CollectionProxy)) |
Instance(model_class.const_get(:ActiveRecord_AssociationRelation)) |
Array.of(Instance(model_class))
end
end
class Foo < ActiveRecord::Base; end
class Bar < ActiveRecord::Base; end
class Thing < Dry::Struct
attribute :foos, Types.ArRelation(Foo)
end
Thing.new(foos: Foo.none) # => A Thing!
Thing.new(foos: Bar.none) # => Dry::Struct::Error: [Thing.new] #<ActiveRecord::Relation []> (Bar::ActiveRecord_Relation) has invalid type for :foos ...
@jfeaver
Copy link
Author

jfeaver commented May 26, 2020

Revision 1: An approach that monkey patches Dry::Types and may break with gem updates.
Revision 2: Uses Dry Validation to specify a contract on the incoming attribute. This implementation could be refactored to make it more repeatable if desired.

Conclusion: It's intentionally difficult to check nested details of objects in Dry Types because it can lead to brittle code that's difficult to maintain. Using a simple type of Types.Instance(ActiveRecord::Relation) or Types::Array.of(Foo) (and calling .to_ary on the relation) will probably be preferred and easier to work with in the end.

Learned afterward: Using the case predicate is what I was originally looking for: Types.Instance(ActiveRecord::Relation).constrained(case: -> rel { rel.klass.equal?(Foo) })

Using the case predicate is good to know about and makes the code brittle but easier to change with less code. flash-gordon pointed this out to me (a dry-rb developer) and warned that it is easy to abuse. So, use with caution and probably avoid until there's a time that feels more right.

And was schooled after all of that: Revision 3

I added some extra cases in Revision 6: Allow for a simple array for flexibility or allow for a collection proxy class which may happen for relations reached through a has_many association (perhaps foo.bars).

@pandwoter
Copy link

Great gist! Thanks!

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