Top Level Namespace

Defined Under Namespace

Modules: Api, BulletTrain, Scaffolding, SuperScaffoldBase, SuperScaffoldingHelper, TerminalCommands Classes: FieldGenerator, IncomingWebhooksGenerator, JoinModelGenerator, OauthProviderGenerator, SuperScaffoldGenerator

Constant Summary collapse

FIELD_PARTIALS =
{
  address_field: nil,
  boolean: "boolean",
  buttons: "string",
  cloudinary_image: "string",
  color_picker: "string",
  date_and_time_field: "datetime",
  date_field: "date",
  email_field: "string",
  emoji_field: "string",
  file_field: "attachment",
  image: "attachment",
  options: "string",
  password_field: "string",
  phone_field: "string",
  super_select: "string",
  text_area: "text",
  text_field: "string",
  number_field: "integer",
  trix_editor: "text"
}

Instance Method Summary collapse

Instance Method Details

#check_required_options_for_attributes(scaffolding_type, attributes, child, parent = nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/scaffolding/script.rb', line 66

def check_required_options_for_attributes(scaffolding_type, attributes, child, parent = nil)
  tableized_parent = nil

  # Ensure the parent attribute name has the proper namespacing for adding as a foreign key.
  if parent.present?
    if child.include?("::") && parent.include?("::")
      child_parts = child.split("::")
      parent_parts = parent.split("::")
      child_parts_dup = child_parts.dup
      parent_parts_dup = parent_parts.dup

      # Pop off however many spaces match.
      child_parts_dup.each.with_index do |child_part, idx|
        if child_part == parent_parts_dup[idx]
          child_parts.shift
          parent_parts.shift
        else
          tableized_parent = parent_parts.map(&:downcase).join("_")
          break
        end
      end
    end
    # In case we're not working with namespaces, just tableize the parent as is.
    tableized_parent ||= parent.tableize.singularize.tr("/", "_") if parent.present?
  end

  generation_command = case scaffolding_type
  when "crud"
    "bin/rails generate model #{child} #{tableized_parent}:references"
  when "crud-field"
    "" # This is blank so we can create the proper migration name first after we get the attributes.
  end

  # Even if there are attributes passed to the scaffolder,
  # They may already exist in previous migrations, so we
  # only register ones that need to be generated.
  # i.e. - *_ids attributes in the join-model scaffolder.
  attributes_to_generate = []

  attributes.each do |attribute|
    parts = attribute.split(":")
    name = parts.shift
    type = parts.join(":")
    type_without_option = type.gsub(/{.*}/, "")

    unless Scaffolding.valid_attribute_type?(type)
      raise "You have entered an invalid attribute type: #{type}. General data types are used when creating new models, but Bullet Train " \
        "uses field partials when Super Scaffolding, i.e. - `name:text_field` as opposed to `name:string`. " \
        "Please refer to the Field Partial documentation to view which attribute types are available."
    end

    # extract any options they passed in with the field.
    type, attribute_options = type.scan(/^(.*){(.*)}/).first || type

    # create a hash of the options.
    attribute_options = if attribute_options
      attribute_options.split(",").map do |s|
        option_name, option_value = s.split("=")
        [option_name.to_sym, option_value || true]
      end.to_h
    else
      {}
    end

    data_type = if type == "image" && cloudinary_enabled?
      "string"
    elsif attribute_options[:multiple]
      case type
      when "file"
        "attachments"
      else
        "jsonb"
      end
    else
      FIELD_PARTIALS[type_without_option.to_sym]
    end

    if name.match?(/_id$/) || name.match?(/_ids$/)
      attribute_options ||= {}
      unless attribute_options[:vanilla]
        name_without_id = if name.match?(/_id$/)
          name.gsub(/_id$/, "")
        elsif name.match?(/_ids$/)
          name.gsub(/_ids$/, "")
        end

        attribute_options[:class_name] ||= name_without_id.classify

        file_name = Dir.glob("app/models/**/*.rb").find { |model| model.match?(/#{attribute_options[:class_name].underscore}\.rb/) } || ""

        # If a model is namespaced, the parent's model file might exist under
        # `app/models/`, but sometimes these files are modules that resolve
        # table names by providing a prefix as opposed to an actual ApplicationRecord.
        # This check ensures that the _id attribute really is a model.
        is_active_record_class = attribute_options[:class_name].constantize.ancestors.include?(ActiveRecord::Base)
        unless File.exist?(file_name) && is_active_record_class
          puts ""
          puts "Attributes that end with `_id` or `_ids` trigger awesome, powerful magic in Super Scaffolding. However, because no `#{attribute_options[:class_name]}` class was found defined in `#{file_name}`, you'll need to specify a `class_name` that exists to let us know what model class is on the other side of the association, like so:".red
          puts ""
          puts "  bin/super-scaffold #{scaffolding_type} #{child}#{" " + parent if parent.present?} #{name}:#{type}{class_name=#{name.gsub(/_ids?$/, "").classify}}".red
          puts ""
          puts "If `#{name}` is just a regular field and isn't backed by an ActiveRecord association, you can skip all this with the `{vanilla}` option, e.g.:".red
          puts ""
          puts "  bin/super-scaffold #{scaffolding_type} #{child}#{" " + parent if parent.present?} #{name}:#{type}{vanilla}".red
          puts ""
          exit
        end
      end
    end

    # TODO: Is there ever a case that we want this to be a string?
    data_type = "references" if name.match?(/_id$/)

    # For join models, we don't want to generate a migration when
    # running the crud-field scaffolder in the last step, so we skip *_ids.
    # Addresses belong_to :addressable, so they don't have to be represented in a migration.
    unless name.match?(/_ids$/) || data_type.nil?
      generation_command += " #{name_without_id || name}:#{data_type}"
      attributes_to_generate << name
    end
  end

  # Generate the models/migrations with the attributes passed.
  if attributes_to_generate.any?
    case scaffolding_type
    # "join-model" is not here because the `rails g` command is written inline in its own scaffolder.
    when "crud"
      puts "Generating #{child} model with '#{generation_command}'".green
    when "crud-field"
      generation_command = "bin/rails generate migration add_#{attributes_to_generate.join("_and_")}_to_#{child.tableize.tr("/", "_")}#{generation_command}"
      puts "Adding new fields to #{child} with '#{generation_command}'".green
    end
    puts ""

    unless @options["skip-migration-generation"]
      untracked_files = has_untracked_files?(git_status) ? get_untracked_files(git_status) : []
      generation_thread = Thread.new { `#{generation_command}` }
      generation_thread.join # Wait for the process to finish.

      newly_untracked_files = has_untracked_files?(git_status) ? get_untracked_files(git_status) : []
      if (newly_untracked_files - untracked_files).size.zero?
        error_message = <<~MESSAGE
          Since you have already created the #{child} model, Super Scaffolding won't allow you to re-create it.
          You can either delete the model and try Super Scaffolding again, or add the `--skip-migration-generation`
          flag to Super Scaffold the classic Bullet Train way.
        MESSAGE
        puts ""
        puts error_message.red
        exit 1
      end
    end
  end
end

#get_untracked_files(status_lines) ⇒ Object

All untracked files begin with a tab (i.e. - “tapp/models/model.rb”).



62
63
64
# File 'lib/scaffolding/script.rb', line 62

def get_untracked_files(status_lines)
  `git ls-files --other --exclude-standard`.split("\n")
end

#git_statusObject



53
54
55
# File 'lib/scaffolding/script.rb', line 53

def git_status
  `git status`.split("\n")
end

#has_untracked_files?(status_lines) ⇒ Boolean

Returns:

  • (Boolean)


57
58
59
# File 'lib/scaffolding/script.rb', line 57

def has_untracked_files?(status_lines)
  status_lines.include?("Untracked files:")
end

#oauth_scaffold_add_line_to_file(file, content, after, options, additional_options = {}) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/scaffolding/oauth_providers.rb', line 101

def oauth_scaffold_add_line_to_file(file, content, after, options, additional_options = {})
  empty_transformer = Scaffolding::Transformer.new("", "")
  file = oauth_transform_string(file, options)
  content = oauth_transform_string(content, options)
  after = oauth_transform_string(after, options)
  empty_transformer.add_line_to_file(file, content, after, additional_options)
end

#oauth_scaffold_directory(directory, options) ⇒ Object



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/scaffolding/oauth_providers.rb', line 1

def oauth_scaffold_directory(directory, options)
  transformed_directory_name = oauth_transform_string(directory, options)
  empty_transformer = Scaffolding::Transformer.new("", "")
  begin
    Dir.mkdir(transformed_directory_name)
  rescue Errno::EEXIST => _
    puts "The directory #{transformed_directory_name} already exists, skipping generation.".yellow
  rescue Errno::ENOENT => _
    puts "Proceeding to generate '#{transformed_directory_name}'."
  end

  Dir.foreach(empty_transformer.resolve_template_path(directory)) do |file|
    file = "#{directory}/#{file}"
    unless File.directory?(empty_transformer.resolve_template_path(file))
      oauth_scaffold_file(file, options)
    end
  end
end

#oauth_scaffold_file(file, options) ⇒ Object

there is a bunch of stuff duplicate here, but i’m OK with that for now.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/scaffolding/oauth_providers.rb', line 21

def oauth_scaffold_file(file, options)
  transformed_file_name = oauth_transform_string(file, options)
  transformed_file_content = []
  empty_transformer = Scaffolding::Transformer.new("", "")

  skipping = false
  File.open(empty_transformer.resolve_template_path(file)).each_line do |line|
    if line.include?("# 🚅 skip when scaffolding.")
      next
    end

    if line.include?("# 🚅 skip this section when scaffolding.")
      skipping = true
      next
    end

    if line.include?("# 🚅 stop any skipping we're doing now.")
      skipping = false
      next
    end

    if skipping
      next
    end

    # remove lines with 'remove in scaffolded files.'
    unless line.include?("remove in scaffolded files.")

      # only transform it if it doesn't have the lock emoji.
      if line.include?("🔒")
        # remove any comments that start with a lock.
        line.gsub!(/\s+?#\s+🔒.*/, "")
      else
        line = oauth_transform_string(line, options)
      end

      transformed_file_content << line

    end
  end
  transformed_file_content = transformed_file_content.join

  transformed_directory_name = File.dirname(transformed_file_name)
  unless File.directory?(transformed_directory_name)
    FileUtils.mkdir_p(transformed_directory_name)
  end

  puts "Writing '#{transformed_file_name}'." unless silence_logs?

  File.write(transformed_file_name, transformed_file_content)
end

#oauth_transform_string(string, options) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/scaffolding/oauth_providers.rb', line 73

def oauth_transform_string(string, options)
  name = options[:our_provider_name]
  empty_transformer = Scaffolding::Transformer.new("", "")

  # get these out of the way first.
  string = string.gsub("stripe_connect: Stripe", empty_transformer.encode_double_replacement_fix(options[:gems_provider_name] + ": " + name.titleize))
  string = string.gsub("ti-money", empty_transformer.encode_double_replacement_fix(options[:icon])) if options[:icon]
  string = string.gsub("omniauth-stripe-connect", empty_transformer.encode_double_replacement_fix(options[:omniauth_gem]))
  string = string.gsub("stripe_connect", empty_transformer.encode_double_replacement_fix(options[:gems_provider_name]))
  string = string.gsub("STRIPE_CLIENT_ID", empty_transformer.encode_double_replacement_fix(options[:api_key]))
  string = string.gsub("STRIPE_SECRET_KEY", empty_transformer.encode_double_replacement_fix(options[:api_secret]))

  # then try for some matches that give us a little more context on what they're looking for.
  string = string.gsub("stripe-account", empty_transformer.encode_double_replacement_fix(name.underscore.dasherize + "_account"))
  string = string.gsub("stripe_account", empty_transformer.encode_double_replacement_fix(name.underscore + "_account"))
  string = string.gsub("StripeAccount", empty_transformer.encode_double_replacement_fix(name + "Account"))
  string = string.gsub("Stripe Account", empty_transformer.encode_double_replacement_fix(name.titleize + " Account"))
  string = string.gsub("Stripe account", empty_transformer.encode_double_replacement_fix(name.titleize + " account"))
  string = string.gsub("with Stripe", empty_transformer.encode_double_replacement_fix("with " + name.titleize))

  # finally, just do the simplest string replace. it's possible this can produce weird results.
  # if they do, try adding more context aware replacements above, e.g. what i did with 'with'.
  string = string.gsub("stripe", empty_transformer.encode_double_replacement_fix(name.underscore))
  string = string.gsub("Stripe", empty_transformer.encode_double_replacement_fix(name))

  empty_transformer.decode_double_replacement_fix(string)
end

#show_usageObject



220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/scaffolding/script.rb', line 220

def show_usage
  puts ""
  puts "🚅  usage: bin/super-scaffold [type] (... | --help | --field-partials)"
  puts ""
  puts "Supported types of scaffolding:"
  puts ""
  BulletTrain::SuperScaffolding.scaffolders.each do |key, _|
    puts "  #{key}"
  end
  puts ""
  puts "Try `bin/super-scaffold [type]` for usage examples.".blue
  puts ""
end

#standard_protipObject



48
49
50
51
# File 'lib/scaffolding/script.rb', line 48

def standard_protip
  puts "🏆 Protip: Commit your other changes before running Super Scaffolding so it's easy to undo if you (or we) make any mistakes."
  puts "If you do that, you can reset to your last commit state by using `git checkout .` and `git clean -d -f` ."
end