Top Level Namespace

Defined Under Namespace

Modules: ActionModels, Api, BulletTrain, Scaffolding, SuperScaffoldBase, SuperScaffoldingHelper, TerminalCommands Classes: ConversationsGenerator, 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_class_name_for_namespace_conflict(class_name) ⇒ Object

class_name is a potentially namespaced class like “Tasks::Widget” or “Task::Widget”. Here we ensure that the namespace doesn’t clobber an existing model. If it does we suggest that the namespace could be pluralized.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/scaffolding/script.rb', line 56

def check_class_name_for_namespace_conflict(class_name)
  if class_name.include?("::")
    parts = class_name.split("::") # ["Task", "Widget"]
    # We drop the last segment because that's tne new model we're trying to create
    parts.pop # ["Task"]
    possible_conflicted_class_name = ""
    parts.each do |part|
      possible_conflicted_class_name += "::#{part}"
      begin
        klass = possible_conflicted_class_name.constantize
        is_active_record_class = klass&.ancestors&.include?(ActiveRecord::Base)
        is_aactive_hash_class = klass&.ancestors&.include?(ActiveHash::Base)
        if klass && (is_active_record_class || is_aactive_hash_class)
          problematic_namespace = possible_conflicted_class_name[2..]
          puts "It looks like the namespace you gave for this model conflicts with an existing class: #{klass.name}".red
          puts "You should use a namespace that doesn't clobber an existing class.".red
          puts ""
          puts "We reccomend using the pluralized version of the existing class.".red
          puts ""
          puts "For instance instead of #{problematic_namespace} use #{problematic_namespace.pluralize}".red
          exit
        end
      rescue NameError
        # this is good actually, it means we don't already have a class that will be clobbered
      end
    end
  end
end

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



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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/scaffolding/script.rb', line 85

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

    if type == "image" && cloudinary_enabled? && attribute_options[:multiple]
      puts "You have Cloudinary enabled and tried to scaffold an image field with the `multiple` option. " \
        "At this time we do not support multiple images in a single Cloudinary image attribute. " \
        "We hope to add support for it in the future. " \
        "For now you could use individual named image attributes, or you might try disabling Cloudinary and using ActiveStorage.".red
      exit
    end

    data_type = if type == "image" && cloudinary_enabled?
      "string"
    elsif attribute_options[:multiple]
      case type
      when "file_field"
        "attachments"
      when "image"
        "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.delete_suffix("_id")
        elsif name.match?(/_ids$/)
          name.delete_suffix("_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/) } || ""

        begin
          class_name_constant = attribute_options[:class_name].constantize
        rescue NameError
          if attribute_options[:class_name] == child
            puts ""
            puts "You appear to be tryingo scaffold a model that references itself. Unfotunately this needs to be a two-step process.".red
            puts "First you should generate the model without the reference, and then add the reference as a new field. For instance:".red
            puts ""
            puts "  rails generate super_scaffold #{child}#{" " + parent if parent.present?}".red
            puts "  rails generate super_scaffold:field #{child} #{name}:#{type}".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 "  rails generate super_scaffold #{child}#{" " + parent if parent.present?} #{name}:#{type}{vanilla}".red
            puts ""
            exit
          else
            # We don't do anything special here because we'll end up triggering the error message below. A self-referential model
            # is kind of a special case that's worth calling out specifically. If we just can't find the model the messaging below
            # should be sufficient to get folks on the right track.
          end
        end

        # 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 = class_name_constant&.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 your app, 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" and "oauth-provider" are not here because the
    # `rails g` command is written inline in their own respective scaffolders.
    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 = get_untracked_files
      generation_thread = Thread.new { `#{generation_command}` }
      generation_thread.join # Wait for the process to finish.

      newly_untracked_files = get_untracked_files
      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_filesObject



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

def get_untracked_files
  `git ls-files --other --exclude-standard`.split("\n")
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