Skip to content

Instantly share code, notes, and snippets.

@hovsater
Last active December 20, 2015 23:38
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 hovsater/6213677 to your computer and use it in GitHub Desktop.
Save hovsater/6213677 to your computer and use it in GitHub Desktop.
Seeda (Design concept)
Seeda.definitions do
# A simple seed
seed { Category.create! name: "Example Category" }
# Seeds can be defined in groups
define :users do
# Seeds can be named for later reference
seed(:john) { User.create! name: "John Doe" }
seed(:jane) { User.create! name: "Jane Doe" }
end
# Seeds can have dependencies
define :posts, [:users] do |users|
seed { users(:john).posts.create! title: "A post" }
seed { users(:jane).posts.create! title: "Another post" }
end
end
@jgaskins
Copy link

I like the idea overall, but it seems kind of verbose at the moment. Right now, you're just wrapping ORM calls with boilerplate. This could be rewritten without Seeda just using ORM calls like this:

category = Category.create! name: 'Example Category'

john = User.create name: 'John Doe'
jane = User.create name: 'Jane Doe'

john.posts.create! title: 'A post'
jane.posts.create! title: 'Another post'

If you pulled out the Model.create! calls, you could probably cut out quite a bit of the verbosity:

Seeda.definitions do
  seed Category, name: 'Example Category'

  define :users do
    seed User, :john, name: 'John Doe' # Defines a `john` method dynamically so we can reference this object below
    seed User, :jane, name: 'Jane Doe'
  end

  define :posts do
    seed john.posts, title: 'A post'
    seed jane.posts, title: 'Another post'
  end
end

This is a little less verbose (though still a bit more verbose than without Seeda) and provides an ORM-agnostic way of inserting the data. I'm not saying this is better all around (I don't know your use cases yet), just that it's an attempt to make it better than doing the ORM calls directly. In order for people to use it, it has to be noticeably better.

I'm also noticing that you could set it to persist multiple objects in define:

Seeda.definitions do
  define :users, count: 100 do |i|
    seed User, name: "User #{i}"
  end
end

Then define :users could define a dynamic method users that returns the array of those 100 users.

I'm just throwing out ideas here. Seeing the code got me thinking. :-)

@hovsater
Copy link
Author

I do understand what you mean. My initial thoughts was similar to yours in regards of wrapping Model.create! statements into the seed method. I didn't want to enforce limitations on the use of the ActiveRecord API by only allowing seed to call out to Model.create!. Consider the following:

seed do
  user = User.new name: "John"
  user.build_profile age: 18, born: "Sweden"
  user.save!
end

This would allow one to create both a user and a profile in the same seed block, if they feel creating separate seeds for profiles is unnecessary. What do you think about this? I like the less verbose version you provided, but in the same time that enforce limitations. Don't you agree?

This could be rewritten without Seeda just using ORM calls [...]

You're right. What Seeda is trying to provide is a structured alternative. You could accomplish the same functionality with simple ORM calls, but that become cluttered pretty fast. Seeda will allow you to split things out into separate files such as db/seeds/users.rb and db/seeds/posts.rb. Could the same be accomplished with simple ORM calls? If so, then I'd have to agree with you that the current implementation is to verbose.

I do like the idea of dynamically creating methods with the name for named seeds. I was thinking about something similar but wanted to keep things simple therefor I decided on injecting the dependencies into the block if they were requested.

One reason to this was the ability to reference unnamed seeds as well. Currently seeds defined in define statements can be referenced through the define name and the name of seed, if a seed doesn't have a name it get assigned a number (which will increment for each unnamed seed). Seeds that isn't wrapped in a define statement, end up in a special group called :__unnamed__.

An example to explain the above:

Seeda.definitions do
  seed { "Seed 1" }

  define :users do
     seed        { "Seed 2" }
     seed(:john) { "Seed 3" }
  end

   define :prove_my_point, [:__unnamed__, :users] do |unnamed, users|
     unnamed(1)   # References "Seed 1"
     users(1)     # References "Seed 2"
     users(:john) # References "Seed 3"
    end
end

I definitely like the idea with define taking a count!

This is me spitting out thoughts 😄

@hovsater
Copy link
Author

One idea that could increase the value of Seeda would be to apply your idea about context.

How about something like this:

Seeda.definitions do
  define :users, User do
    # A seed without arguments will execute it's content in the User context.
    seed { create! name: "John Doe" }

    # A seed with arguments will execute it's content in the User.new context.
    seed do |user|
      user.name = 'John Doe'
      user.save!
    end    
  end
end

@jgaskins
Copy link

I like how this cleans up the multiple calls to User.create!, which is great if you change the class name to Member or something else more descriptive or humane. A lot of people have objections to the word "User" as a class name representing people using your app.

This could easily be implemented. Blocks become Procs, which have an arity attribute, so you could branch on block.arity being 0 or 1. Run instance_exec or class_exec if it's 0 (class_exec would be best unless you plan on being able to pass in non-Class objects to define) and yield model_class.new if it's 1.

I do wonder if {class,instance}_exec would be too much magic here, though. For example, this line:

seed { create! name: "John Doe" }

When someone first looks at that, they might think create! was a method on the definition. Maybe seed { model.create! foo: 'bar' } might be better and the block is simply evaluated in the context of the current object, with model being the class/object passed into define.

Don't get me wrong, I love that kind of magic. I used instance_exec everywhere in Perpetuity at first. But when explaining the gem to other people, I found I was having to justify that magic a lot, which I thought was kind of a UX smell. And it turned out, using yield in the query DSL was much better anyway (gave it a more Enumerable-like API).

@dbrady
Copy link

dbrady commented Aug 12, 2013

I don't have much to add other than I like the direction you're heading. There was a WONDERFUL baby that got thrown out with the bathwater called FixJour a few years ago. The thing I liked about it was that you could build complicated nested objects by default, E.g. build :user would return a User with an associated default Profile, but if you specified a profile object in the build call, it would seamlessly override the default. This let you do things like spec a user with a nil/missing profile just by calling build :user, profile: nil, etc.

Anyway, my point is you might want to look at FJ, but if not, please do consider making it easy to override seeds from the test suite UNLESS OF COURSE that's sort of not your point in creating Seeda in the first place. :-)

@hovsater
Copy link
Author

@jgaskins I feel the same way. I do love the magic that class_exec and instance_exec provide, but I don't think it's worth it if people having a hard time understanding what context the methods are executed in. At first thought, I don't think the magic happening here is too confusing, but on the other hand, a new user to gem might think so.

I guess it's a decision that have to be made on whether to use yield or class_exec and instance_exec.

One thought that popped into mind was to convert the name of the definition into singular and then use it like so: seed { user.create! foo: bar } but I think that is too much trouble of converting the name into singular as well as the confusion with the seed syntax that takes an argument.

@dbrady I will definitely look into FixJour and see if I can get some inspiration!

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