Code Generation with Super Scaffolding
Super Scaffolding is Bullet Train’s code generation engine. Its goal is to allow you to produce production-ready CRUD interfaces for your models while barely lifting a finger, and it handles a lot of other grunt-work as well.
Here’s a list of what Super Scaffolding takes care of for you each time you add a model to your application:
- It generates a basic CRUD controller and accompanying views.
- It generates a Yaml locale file for the views’ translatable strings.
- It generates type-specific form fields for each attribute of the model.
- It generates an API controller and an accompanying entry in the application’s API docs.
- It generates a serializer that’s used by the API and when dispatching webhooks.
- It adds the appropriate permissions for multitenancy in CanCanCan’s configuration file.
- It adds the model’s table view to the show view of its parent.
- It adds the model to the application’s navigation (if applicable).
- It generates breadcrumbs for use in the application’s layout.
- It generates the appropriate routes for the CRUD controllers and API endpoints.
When adding just one model, Super Scaffolding generates ~30 different files on your behalf.
Living Templates
Bullet Train's Super Scaffolding engine is a unique approach to code generation, based on template files that are functional code instead of obscure DSLs that are difficult to customize and maintain. Super Scaffolding automates the most repetitive and mundane aspects of building out your application's basic structure. Furthermore, it does this without leaning on the magic of libraries that force too high a level of abstraction. Instead, it generates standard Rails code that is both ready for prime time, but is also easy to customize and modify.
Prerequisites
Before getting started with Super Scaffolding, we recommend reading about the philosophy of domain modeling in Bullet Train.
Usage
The Super Scaffolding shell script provides its own documentation. If you're curious about specific scaffolders or parameters, you can run the following in your shell:
rails generate super_scaffold
Available Scaffolding Types
rails generate Command |
Scaffolding Type |
---|---|
rails generate super_scaffold |
Basic CRUD scaffolder |
rails generate super_scaffold:field |
Adds a field to an existing model |
rails generate super_scaffold:incoming_webhook |
Scaffolds an incoming webhook |
rails generate super_scaffold:join_model |
Scaffolds a join model (must have two existing models to join before scaffolding) |
rails generate super_scaffold:oauth_provider |
Scaffolds logic to use OAuth2 with the provider of your choice |
The following commands are for use specifically with Action Models.
rails generate Command |
Scaffolding Type |
---|---|
rails generate super_scaffold:action_models:targets_many |
Generates an action that targets many records |
rails generate super_scaffold:action_models:targets_one |
Generates an action that targets one record |
rails generate super_scaffold:action_models:targets_one_parent |
Generates an action that targets the parent of the specified model |
Examples
1. Basic CRUD Scaffolding
Let's implement the following feature:
An organization has many projects.
First, run the scaffolder:
rails generate super_scaffold Project Team name:text_field
rake db:migrate
In the above example, team
represents the model that a Project
primarily belongs to. Also, text_field
was selected from the list of available field partials. We'll show examples with trix_editor
and super_select
later.
Super Scaffolding automatically generates models for you. However, if you want to split this process, you can pass the --skip-migration-generation
to the command.
For example, generate the model with the standard Rails generator:
rails g model Project team:references name:string
⚠️ Don't run migrations right away. It would be fine in this case, but sometimes the subsequent Super Scaffolding step actually updates the migration as part of its magic.
Then you can run the scaffolder with the flag:
rails generate super_scaffold Project Team name:text_field --skip-migration-generation
2. Nested CRUD Scaffolding
Building on that example, let's implement the following feature:
A project has many goals.
First, run the scaffolder:
rails generate super_scaffold Goal Project,Team description:text_field
rake db:migrate
You can see in the example above how we've specified Project,Team
, because we want to specify the entire chain of ownership back to the Team
. This allows Super Scaffolding to automatically generate the required permissions. Take note that this generates a foreign key for Project
and not for Team
.
field
3. Adding New Fields with One of Bullet Train's most valuable features is the ability to add new fields to existing scaffolded models. When you add new fields with the field
scaffolder, you don't have to remember to add that same attribute to table views, show views, translation files, API endpoints, serializers, tests, documentation, etc.
Building on the earlier example, consider the following new requirement:
In addition to a name, a project can have a description.
Use the field
scaffolder to add it throughout the application:
rails generate super_scaffold:field Project description:trix_editor
rake db:migrate
As you can see, when we're using field
, we don't need to supply the chain of ownership back to Team
.
If you want to scaffold a new field to use for read-only purposes, add the following option to omit the field from the form and all other files that apply:
rails generate super_scaffold:field Project description:trix_editor{readonly}
Again, if you would like to automatically generate the migration on your own, pass the --skip-migration-generation
flag:
rails generate super_scaffold:field Project description:trix_editor --skip-migration-generation
4. Adding Option Fields with Fixed, Translatable Options
Continuing with the earlier example, let's address the following new requirement:
Users can specify the current project status.
We have multiple field partials that we could use for this purpose, including buttons
, options
, or super_select
.
In this example, let's add a status attribute and present it as buttons:
rails generate super_scaffold:field Project status:buttons
By default, Super Scaffolding configures the buttons as "One", "Two", and "Three", but in this example you can edit those options in the fields
section of config/locales/en/projects.en.yml
. For example, you could specify the following options:
planned: Planned
started: Started
completed: Completed
If you want new Project
models to be set to planned
by default, you can add that to the migration file that was generated before running it, like so:
add_column :projects, :status, :string, default: "planned"
belongs_to
Associations, Team Member Assignments
5. Scaffolding Continuing with the example, consider the following requirement:
A project has one specific project lead.
Although you might think this calls for a reference to User
, we've learned the hard way that it's typically much better to assign resources on a Team
to a Membership
on the team instead. For one, this allows you to assign resources to new team members that haven't accepted their invitation yet (and don't necessarily have a User
record yet.)
We can accomplish this like so:
rails generate super_scaffold:field Project lead_id:super_select{class_name=Membership}
rake db:migrate
There are three important things to point out here:
- The scaffolder automatically adds a foreign key for
lead
toProject
. - When adding this foreign key the
references
column is generated under the namelead
, but when we're specifying the field we want to scaffold, we specify it aslead_id
, because that's the name of the attribute on the form, in strong parameters, etc. - We have to specify the model name with the
class_name
option so that Super Scaffolding can fully work it's magic. We can't reflect on the association, because at this point the association isn't properly defined yet. With this information, Super Scaffolding can handle that step for you.
Finally, Super Scaffolding will prompt you to edit app/models/project.rb
and implement the required logic in the valid_leads
method. This is a template method that will be used to both populate the select field on the Project
form, but also enforce some important security concerns in this multi-tenant system. In this case, you can define it as:
def valid_leads
team.memberships.current_and_invited
end
(The current_and_invited
scope just filters out people that have already been removed from the team.)
join_model
6. Scaffolding Has-Many-Through Associations with Finally, working from the same example, imagine the following requirement:
A project can be labeled with one or more project-specific tags.
We can accomplish this with a new model, a new join model, and a super_select
field.
First, let's create the tag model:
rails generate super_scaffold Projects::Tag Team name:text_field
Note that project tags are specifically defined at the Team
level. The same tag can be applied to multiple Project
models.
Now, let's create a join model for the has-many-through association.
We're not going to scaffold this model with the typical rails generate super_scaffold
scaffolder, but some preparation is needed before we can use it with the field
scaffolder, so we need to do the following:
rails generate super_scaffold:join_model Projects::AppliedTag project_id{class_name=Project} tag_id{class_name=Projects::Tag}
All we're doing here is specifying the name of the join model, and the two attributes and class names of the models it joins. Note again that we specify the _id
suffix on both of the attributes.
Now that the join model has been prepared, we can use the field
scaffolder to create the multi-select field:
rails generate super_scaffold:field Project tag_ids:super_select{class_name=Projects::Tag}
rake db:migrate
Just note that the suffix of the field is _ids
plural, and this is an attribute provided by Rails to interact with the has_many :tags, through: :applied_tags
association.
The field
step will ask you to define the logic for the valid_tags
method in app/models/project.rb
. You can define it like so:
def valid_tags
team.projects_tags
end
Honestly, it's crazy that we got to the point where we can handle this particular use case automatically. It seems simple, but there is so much going on to make this feature work.
7. Scaffolding image upload attributes
Bullet Train comes with two different ways to handle image uploads.
- Cloudinary - This option allows your app deployment to be simpler because you don't need to ship any image manipulation libraries. But it does introduce a dependence on a 3rd party service.
- ActiveStorage - This option doesn't include reliance on a 3rd party service, but you do have to include image manipulation libararies in your deployment process.
Scaffolding images with Cloudinary
When you scaffold your model a string
is generated where Cloudinary can store a reference to the image.
Make sure you have the CLOUDINARY_URL
environment variable to use Cloudinary for your images.
For instance to scaffold a Project
model with a logo
image upload.
Use image
as a field type for super scaffolding:
rails generate super_scaffold Project Team name:text_field logo:image
rake db:migrate
Under the hood, Bullet Train will generate your model with the following command:
rails generate super_scaffold Project Team name:text_field
rake db:migrate
Scaffolding images with ActiveStorage
When you scaffold your model we generate an attachment
type attribute.
For instance to scaffold a Project
model with a logo
image upload.
Use image
as a field type for super scaffolding:
rails generate super_scaffold Project Team name:text_field logo:image
rake db:migrate
Under the hood, Bullet Train will generate your model with the following command:
rails generate super_scaffold Project Team name:text_field
rake db:migrate
Additional Notes
TangibleThing
and CreativeConcept
In order to properly facilitate this type of code generation, Bullet Train includes two models in the Scaffolding
namespace as a parent and child model:
Scaffolding::AbsolutelyAbstract::CreativeConcept
Scaffolding::CompletelyConcrete::TangibleThing
Their peculiar naming is what's required to ensure that their corresponding view and controller templates can serve as the basis for any combination of different model naming or namespacing that you may need to employ in your own application. There are a ton of different potential combinations of parent and child namespaces, and these two class names provide us with the fidelity we need when transforming the templates to represent any of these scenarios.
Only the files associated with Scaffolding::CompletelyConcrete::TangibleThing
actually serve as scaffolding templates, so we also take advantage of Scaffolding::AbsolutelyAbstract::CreativeConcept
to demonstrate other available Bullet Train features. For example, we use it to demonstrate how to implement resource-level collaborators.
Hiding Scaffolding Templates
You won't want your end users seeing the Super Scaffolding templates in your environment, so you can disable their presentation by setting HIDE_THINGS
in your environment. For example, you can add the following to config/application.yml
:
HIDE_THINGS: true