ruby_ui_scaffold 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +343 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +530 -0
  5. data/lib/generators/ruby_ui_scaffold/install/install_generator.rb +188 -0
  6. data/lib/generators/ruby_ui_scaffold/ruby_ui_scaffold_generator.rb +119 -0
  7. data/lib/generators/ruby_ui_scaffold/scaffold/scaffold_generator.rb +252 -0
  8. data/lib/generators/ruby_ui_scaffold/scaffold/templates/edit.rb.tt +34 -0
  9. data/lib/generators/ruby_ui_scaffold/scaffold/templates/form.rb.tt +50 -0
  10. data/lib/generators/ruby_ui_scaffold/scaffold/templates/index.rb.tt +108 -0
  11. data/lib/generators/ruby_ui_scaffold/scaffold/templates/index_data_table.rb.tt +187 -0
  12. data/lib/generators/ruby_ui_scaffold/scaffold/templates/new.rb.tt +34 -0
  13. data/lib/generators/ruby_ui_scaffold/scaffold/templates/show.rb.tt +55 -0
  14. data/lib/generators/ruby_ui_scaffold/scaffold_controller/scaffold_controller_generator.rb +43 -0
  15. data/lib/generators/ruby_ui_scaffold/scaffold_controller/templates/controller.rb.tt +75 -0
  16. data/lib/generators/ruby_ui_scaffold/scaffold_controller/templates/controller_data_table.rb.tt +110 -0
  17. data/lib/rails/commands/ruby_ui_scaffold/seed_command.rb +62 -0
  18. data/lib/ruby_ui_scaffold/attribute_helpers.rb +38 -0
  19. data/lib/ruby_ui_scaffold/component_installer.rb +24 -0
  20. data/lib/ruby_ui_scaffold/component_resolver.rb +74 -0
  21. data/lib/ruby_ui_scaffold/field_type_mapper.rb +164 -0
  22. data/lib/ruby_ui_scaffold/railtie.rb +25 -0
  23. data/lib/ruby_ui_scaffold/seeder.rb +115 -0
  24. data/lib/ruby_ui_scaffold/value_generator.rb +168 -0
  25. data/lib/ruby_ui_scaffold/version.rb +5 -0
  26. data/lib/ruby_ui_scaffold.rb +22 -0
  27. metadata +197 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ # Shared helpers used by both the scaffold_controller and views generators
5
+ # to identify which attributes participate in sort and search.
6
+ module AttributeHelpers
7
+ EXCLUDED_FROM_SORT = %i[text rich_text json jsonb binary attachment attachments].freeze
8
+
9
+ # Columns we'll allowlist for sorting. Excludes large/blob types and
10
+ # password digests, since sorting on them makes no sense.
11
+ def sortable_columns
12
+ attributes.reject { |a|
13
+ EXCLUDED_FROM_SORT.include?(a.type) ||
14
+ (a.respond_to?(:password_digest?) && a.password_digest?) ||
15
+ (a.respond_to?(:attachment?) && (a.attachment? || a.attachments?))
16
+ }.map(&:column_name)
17
+ end
18
+
19
+ # Only string columns are searchable via a LIKE clause. Excludes
20
+ # password_digest. Boolean/integer/date columns aren't useful with LIKE.
21
+ def searchable_columns
22
+ attributes.select { |a|
23
+ a.type == :string &&
24
+ !(a.respond_to?(:password_digest?) && a.password_digest?)
25
+ }.map(&:column_name)
26
+ end
27
+
28
+ # Non-polymorphic belongs_to attributes — used to:
29
+ # 1. Eager-load via `scope.includes(*reference_associations)` in the controller
30
+ # 2. Display friendly labels (assoc.name vs assoc_id) in index/show
31
+ def reference_associations
32
+ attributes.select do |a|
33
+ a.respond_to?(:reference?) && a.reference? &&
34
+ !(a.respond_to?(:polymorphic?) && a.polymorphic?)
35
+ end.map { |a| a.name.to_sym }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ # Shared helpers for generators that install ruby_ui components. Mixed into
5
+ # both the install generator (base set) and the scaffold generator
6
+ # (column/flag-specific set). Relies on the including class exposing
7
+ # `destination_root` (every Rails generator does).
8
+ module ComponentInstaller
9
+ # A ruby_ui component is considered installed once its package directory
10
+ # (or single-file component) exists under app/components/ruby_ui/. Guarding
11
+ # on this is essential: `ruby_ui:component` copies files WITHOUT --force,
12
+ # so re-installing a present component prompts interactively — which would
13
+ # hang a non-interactive subprocess.
14
+ def component_installed?(name)
15
+ base = File.join(destination_root, "app/components/ruby_ui", name)
16
+ Dir.exist?(base) || File.exist?("#{base}.rb")
17
+ end
18
+
19
+ # The subset of `names` not yet installed, preserving order.
20
+ def uninstalled_components(names)
21
+ names.reject { |name| component_installed?(name) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ # Resolves which ruby_ui components a scaffold references, given its
5
+ # attributes and options. Only DIRECT references are listed — the
6
+ # `ruby_ui:component NAME` generator resolves transitive dependencies
7
+ # itself (e.g. `date_picker` pulls `calendar` + `popover` + `input`;
8
+ # `data_table` pulls `table`, `checkbox`, `native_select`, `pagination`,
9
+ # `dropdown_menu`, `input`, `button`). See ruby_ui's `dependencies.yml`.
10
+ #
11
+ # @example
12
+ # RubyUiScaffold::ComponentResolver.call(attributes: attrs, datatable: false)
13
+ # # => ["alert_dialog", "badge", "button", ...]
14
+ module ComponentResolver
15
+ # Components every generated scaffold uses, regardless of columns/flags.
16
+ # The install generator pre-installs these so a bare `:install` leaves the
17
+ # ground ready; the scaffold then adds the column/flag-specific ones.
18
+ #
19
+ # index → table, link, button, dropdown_menu, alert_dialog
20
+ # show → card, typography (Text), link, button
21
+ # form → form (FormField*), input, button
22
+ BASE = %w[
23
+ table
24
+ link
25
+ button
26
+ card
27
+ typography
28
+ dropdown_menu
29
+ alert_dialog
30
+ form
31
+ input
32
+ ].freeze
33
+
34
+ module_function
35
+
36
+ # The full set of ruby_ui component generator names a scaffold with these
37
+ # attributes/options references — BASE plus the column/flag conditionals.
38
+ # Returns a sorted, de-duplicated array.
39
+ def call(attributes:, datatable: false)
40
+ components = BASE.dup
41
+ components << "data_table" if datatable
42
+
43
+ attributes.each do |attribute|
44
+ components.concat(components_for_attribute(attribute))
45
+ end
46
+
47
+ components.uniq.sort
48
+ end
49
+
50
+ # Conditional components a single attribute pulls in:
51
+ # boolean → Badge (index/show) + Checkbox (form)
52
+ # text → Textarea (form)
53
+ # reference → Combobox + Select (form) — polymorphic falls back to a
54
+ # plain number Input, so it adds nothing
55
+ # date → DatePicker (form)
56
+ # Everything else (string/integer/float/decimal/time/datetime/password/
57
+ # attachment/polymorphic reference) maps to the base `input`.
58
+ def components_for_attribute(attribute)
59
+ return %w[combobox select] if reference?(attribute)
60
+
61
+ case attribute.type
62
+ when :boolean then %w[badge checkbox]
63
+ when :text then %w[textarea]
64
+ when :date then %w[date_picker]
65
+ else []
66
+ end
67
+ end
68
+
69
+ def reference?(attribute)
70
+ attribute.respond_to?(:reference?) && attribute.reference? &&
71
+ !(attribute.respond_to?(:polymorphic?) && attribute.polymorphic?)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ # Maps a Rails::Generators::GeneratedAttribute to a ruby_ui input
5
+ # component, emitted as a Ruby code snippet for interpolation into
6
+ # a Phlex view template.
7
+ #
8
+ # Multi-line snippets are returned with newline separators and NO
9
+ # leading indentation; the caller is responsible for indenting
10
+ # subsequent lines to match its context. The generator wraps this
11
+ # via `ruby_ui_input_for(attribute, indent:)`.
12
+ #
13
+ # @example
14
+ # RubyUiScaffold::FieldTypeMapper.render(attribute, model_var: "user")
15
+ # # => 'Input(type: "text", id: "user_name", name: "user[name]", value: @user.name)'
16
+ class FieldTypeMapper
17
+ def self.render(attribute, model_var:)
18
+ new(attribute, model_var).render
19
+ end
20
+
21
+ def initialize(attribute, model_var)
22
+ @attr = attribute
23
+ @model_var = model_var.to_s
24
+ end
25
+
26
+ def render
27
+ return password_input if @attr.respond_to?(:password_digest?) && @attr.password_digest?
28
+ return file_input if @attr.respond_to?(:attachment?) && (@attr.attachment? || @attr.attachments?)
29
+ return reference_input if @attr.respond_to?(:reference?) && @attr.reference?
30
+
31
+ type_input
32
+ end
33
+
34
+ private
35
+
36
+ def type_input
37
+ case @attr.type
38
+ when :text then textarea
39
+ when :boolean then checkbox
40
+ when :integer then input("number", step: 1)
41
+ when :float, :decimal then input("number", step: "any")
42
+ when :date then date_picker
43
+ when :time then input("time", value_suffix: %q{&.strftime("%H:%M")})
44
+ when :datetime, :timestamp then input("datetime-local", value_suffix: %q{&.strftime("%Y-%m-%dT%H:%M")})
45
+ else input("text")
46
+ end
47
+ end
48
+
49
+ def id_attr
50
+ "#{@model_var}_#{@attr.column_name}"
51
+ end
52
+
53
+ def name_attr
54
+ "#{@model_var}[#{@attr.column_name}]"
55
+ end
56
+
57
+ def value_ref
58
+ "@#{@model_var}.#{@attr.column_name}"
59
+ end
60
+
61
+ def input(type, step: nil, value_suffix: nil)
62
+ value_expr = value_suffix ? "#{value_ref}#{value_suffix}" : value_ref
63
+ step_part = step ? %Q{, step: #{step.is_a?(String) ? %Q{"#{step}"} : step}} : ""
64
+ %Q{Input(type: "#{type}", id: "#{id_attr}", name: "#{name_attr}", value: #{value_expr}#{step_part})}
65
+ end
66
+
67
+ # ruby_ui DatePicker renders its own submittable <input> internally (name/
68
+ # value), wrapped in a Popover + Calendar. We pass the Date straight to
69
+ # `selected_date:` — the component derives the input's string value via
70
+ # `selected_date.to_s`, which is already ISO (yyyy-MM-dd), matching its
71
+ # default `date_format`. `label: nil` suppresses the component's built-in
72
+ # label, since the form template already emits a FormFieldLabel.
73
+ def date_picker
74
+ %Q{DatePicker(id: "#{id_attr}", name: "#{name_attr}", selected_date: #{value_ref}, label: nil)}
75
+ end
76
+
77
+ def textarea
78
+ <<~RUBY.chomp
79
+ Textarea(rows: 4, id: "#{id_attr}", name: "#{name_attr}") do
80
+ #{value_ref}.to_s
81
+ end
82
+ RUBY
83
+ end
84
+
85
+ def checkbox
86
+ <<~RUBY.chomp
87
+ input(type: "hidden", name: "#{name_attr}", value: "0")
88
+ Checkbox(id: "#{id_attr}", name: "#{name_attr}", value: "1", checked: !!#{value_ref})
89
+ RUBY
90
+ end
91
+
92
+ def password_input
93
+ %Q{Input(type: "password", id: "#{id_attr}", name: "#{name_attr}", value: "")}
94
+ end
95
+
96
+ def file_input
97
+ if @attr.attachments?
98
+ %Q{Input(type: "file", id: "#{id_attr}", name: "#{@model_var}[#{@attr.column_name}][]", multiple: true)}
99
+ else
100
+ %Q{Input(type: "file", id: "#{id_attr}", name: "#{name_attr}")}
101
+ end
102
+ end
103
+
104
+ def reference_input
105
+ if @attr.respond_to?(:polymorphic?) && @attr.polymorphic?
106
+ return polymorphic_reference_input
107
+ end
108
+
109
+ target_class = @attr.name.to_s.classify
110
+ assoc_name = @attr.name
111
+ <<~RUBY.chomp
112
+ if #{target_class}.count > COMBOBOX_THRESHOLD
113
+ # Searchable Combobox for large parent lists
114
+ # The Combobox controller auto-syncs trigger text from the checked
115
+ # radio on connect (`updateTriggerContent`), so we only need to mark
116
+ # the current selection via `checked:`.
117
+ Combobox do
118
+ ComboboxTrigger(placeholder: "Select #{target_class}")
119
+ ComboboxPopover do
120
+ ComboboxSearchInput(placeholder: "Search #{target_class}...")
121
+ ComboboxList do
122
+ #{target_class}.all.each do |record|
123
+ ComboboxItem do
124
+ ComboboxRadio(value: record.id.to_s, name: "#{name_attr}", checked: record.id == #{value_ref})
125
+ span { (record.try(:name) || record.try(:title) || record.try(:display_name) || "#{target_class} \#{record.id}").to_s }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ else
132
+ # Plain Select for small lists. Unlike Combobox, the Select controller
133
+ # does NOT auto-sync the trigger label from the hidden input on connect,
134
+ # so we render the current selection's label inline (via block on
135
+ # SelectValue) and mark the matching SelectItem with aria_selected.
136
+ current_#{assoc_name}_label = if @#{@model_var}.#{assoc_name}
137
+ assoc = @#{@model_var}.#{assoc_name}
138
+ (assoc.try(:name) || assoc.try(:title) || assoc.try(:display_name) || "#{target_class} \#{assoc.id}").to_s
139
+ end
140
+ Select do
141
+ SelectInput(name: "#{name_attr}", value: #{value_ref})
142
+ SelectTrigger do
143
+ SelectValue(placeholder: "Select #{target_class}") { current_#{assoc_name}_label }
144
+ end
145
+ SelectContent do
146
+ #{target_class}.all.each do |record|
147
+ SelectItem(value: record.id.to_s, aria_selected: (record.id == #{value_ref}).to_s) do
148
+ (record.try(:name) || record.try(:title) || record.try(:display_name) || "#{target_class} \#{record.id}").to_s
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ RUBY
155
+ end
156
+
157
+ def polymorphic_reference_input
158
+ <<~RUBY.chomp
159
+ # TODO: polymorphic association — fill in target type and id manually
160
+ Input(type: "number", id: "#{id_attr}", name: "#{name_attr}", value: #{value_ref})
161
+ RUBY
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ class Railtie < ::Rails::Railtie
5
+ # The `generators` block runs only when Rails actually needs generators
6
+ # (i.e. on `rails g`), not at app boot — important because
7
+ # Rails::Generators isn't loaded at boot.
8
+ generators do
9
+ # IMPORTANT: set option defaults BEFORE requiring the generator
10
+ # classes. `class_option(...)` (called by `hook_for`) freezes the
11
+ # default at class-definition time via `default_value_for_option`,
12
+ # which reads from Rails::Generators.options.
13
+ #
14
+ # We only need to override template_engine — the scaffold_controller
15
+ # default falls back to :scaffold_controller (Rails-wide), which
16
+ # combined with our base_name routes correctly to ours.
17
+ ::Rails::Generators.options[:ruby_ui_scaffold] ||= {}
18
+ ::Rails::Generators.options[:ruby_ui_scaffold][:template_engine] = "ruby_ui_scaffold"
19
+
20
+ require "generators/ruby_ui_scaffold/ruby_ui_scaffold_generator"
21
+ require "generators/ruby_ui_scaffold/scaffold_controller/scaffold_controller_generator"
22
+ require "generators/ruby_ui_scaffold/scaffold/scaffold_generator"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_ui_scaffold/value_generator"
4
+
5
+ module RubyUiScaffold
6
+ class SeederError < StandardError; end
7
+
8
+ # Orchestrates seeding N records for a given ActiveRecord model.
9
+ # Delegates per-attribute value generation to ValueGenerator.
10
+ #
11
+ # @example
12
+ # seeder = RubyUiScaffold::Seeder.new(Buddy, count: 50)
13
+ # seeder.run # => prints progress, returns count of created records
14
+ class Seeder
15
+ MAX_RETRIES = 3
16
+ PROGRESS_EVERY = 10
17
+
18
+ def initialize(model_class, count:, reset: false, dry_run: false, io: $stdout)
19
+ @model_class = model_class
20
+ @count = count
21
+ @reset = reset
22
+ @dry_run = dry_run
23
+ @io = io
24
+ @created = 0
25
+ @failed = 0
26
+ @errors = []
27
+ end
28
+
29
+ def run
30
+ return dry_run! if @dry_run
31
+
32
+ preflight!
33
+ reset_table! if @reset
34
+
35
+ started = Time.now
36
+ @io.puts "Seeding #{@count} #{@model_class} records..."
37
+
38
+ @count.times do |i|
39
+ record = attempt_create
40
+ if record
41
+ @created += 1
42
+ report_progress(i + 1, record)
43
+ else
44
+ @failed += 1
45
+ end
46
+ end
47
+
48
+ report_summary(Time.now - started)
49
+ @created
50
+ end
51
+
52
+ private
53
+
54
+ def attempt_create
55
+ MAX_RETRIES.times do
56
+ attrs = ValueGenerator.attributes_for(@model_class)
57
+ record = @model_class.new(attrs)
58
+ return record if record.save
59
+
60
+ @errors << record.errors.full_messages.join(", ")
61
+ end
62
+ nil
63
+ end
64
+
65
+ def preflight!
66
+ @model_class.reflect_on_all_associations(:belongs_to).each do |assoc|
67
+ next if assoc.options[:polymorphic]
68
+ next if assoc.options[:optional]
69
+ next if assoc.klass.unscoped.exists?
70
+
71
+ raise SeederError,
72
+ "#{@model_class} requires #{assoc.klass} records first. " \
73
+ "Run `rails ruby_ui_scaffold:seed #{assoc.klass} --count 10` first."
74
+ end
75
+ end
76
+
77
+ def reset_table!
78
+ @io.puts "Resetting #{@model_class}.destroy_all..."
79
+ @model_class.destroy_all
80
+ end
81
+
82
+ def dry_run!
83
+ sample = ValueGenerator.attributes_for(@model_class)
84
+ @io.puts "Dry run — would create #{@model_class} with attributes:"
85
+ sample.each { |k, v| @io.puts " #{k}: #{v.inspect}" }
86
+ 0
87
+ end
88
+
89
+ def report_progress(idx, record)
90
+ return unless (idx % PROGRESS_EVERY).zero? || idx == @count
91
+
92
+ label = display_label(record)
93
+ @io.puts " [#{idx}/#{@count}] last: #{@model_class}(id: #{record.id}, #{label})"
94
+ end
95
+
96
+ def display_label(record)
97
+ %i[name title display_name email].each do |attr|
98
+ next unless record.respond_to?(attr)
99
+
100
+ value = record.public_send(attr)
101
+ return "#{attr}: #{value.inspect}" if value.present?
102
+ end
103
+ "id: #{record.id}"
104
+ end
105
+
106
+ def report_summary(elapsed)
107
+ @io.puts
108
+ @io.puts "Created #{@created} of #{@count} #{@model_class} records in #{elapsed.round(2)}s."
109
+ return if @failed.zero?
110
+
111
+ @io.puts "Skipped: #{@failed}. First validation errors:"
112
+ @errors.uniq.first(3).each { |e| @io.puts " - #{e}" }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "date"
5
+ require "faker"
6
+
7
+ module RubyUiScaffold
8
+ # Produces a single attribute value for a given ActiveRecord column,
9
+ # using an inference chain: belongs_to FK → enum → inclusion validator →
10
+ # name-based heuristic → type-based fallback.
11
+ #
12
+ # Faker is a runtime dependency of this gem, so realistic fake data
13
+ # (names, emails, addresses, etc.) is always available.
14
+ #
15
+ # @example Build a full attribute hash for a model
16
+ # attrs = RubyUiScaffold::ValueGenerator.attributes_for(User)
17
+ # User.new(attrs)
18
+ class ValueGenerator
19
+ SKIPPED_COLUMN_SUFFIXES = %w[_count].freeze
20
+ SKIPPED_COLUMN_NAMES = %w[id created_at updated_at].freeze
21
+
22
+ class << self
23
+ # @param model_class [Class] an ActiveRecord::Base subclass
24
+ # @return [Hash{String => Object}] attributes safe to assign via Model.new
25
+ def attributes_for(model_class)
26
+ seedable_columns(model_class).each_with_object({}) do |column, hash|
27
+ hash[column.name] = new(column, model_class).call
28
+ end
29
+ end
30
+
31
+ def seedable_columns(model_class)
32
+ inheritance_column = model_class.inheritance_column
33
+ polymorphic_types = polymorphic_type_columns(model_class)
34
+
35
+ model_class.columns.reject do |c|
36
+ SKIPPED_COLUMN_NAMES.include?(c.name) ||
37
+ SKIPPED_COLUMN_SUFFIXES.any? { |s| c.name.end_with?(s) } ||
38
+ c.name == inheritance_column ||
39
+ polymorphic_types.include?(c.name)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def polymorphic_type_columns(model_class)
46
+ model_class.reflect_on_all_associations(:belongs_to).filter_map do |a|
47
+ "#{a.name}_type" if a.options[:polymorphic]
48
+ end
49
+ end
50
+ end
51
+
52
+ def initialize(column, model_class)
53
+ @column = column
54
+ @model_class = model_class
55
+ end
56
+
57
+ def call
58
+ belongs_to_value ||
59
+ enum_value ||
60
+ inclusion_value ||
61
+ numericality_value ||
62
+ name_based_value ||
63
+ type_based_value
64
+ end
65
+
66
+ private
67
+
68
+ # --- inference layers ---
69
+
70
+ def belongs_to_value
71
+ assoc = @model_class.reflect_on_all_associations(:belongs_to)
72
+ .find { |a| !a.options[:polymorphic] && a.foreign_key == @column.name }
73
+ return nil unless assoc
74
+
75
+ ids = assoc.klass.unscoped.ids
76
+ return ids.sample if ids.any?
77
+
78
+ raise SeederError,
79
+ "#{@model_class} belongs_to :#{assoc.name} but no #{assoc.klass} records exist. " \
80
+ "Run `rails ruby_ui_scaffold:seed #{assoc.klass} --count 10` first."
81
+ end
82
+
83
+ def enum_value
84
+ enum = (@model_class.defined_enums || {})[@column.name]
85
+ enum&.keys&.sample
86
+ end
87
+
88
+ def inclusion_value
89
+ v = @model_class.validators_on(@column.name).find do |x|
90
+ x.is_a?(ActiveModel::Validations::InclusionValidator)
91
+ end
92
+ return nil unless v
93
+
94
+ list = v.options[:in] || v.options[:within]
95
+ return nil unless list.respond_to?(:to_a)
96
+
97
+ list.to_a.sample
98
+ end
99
+
100
+ def numericality_value
101
+ return nil unless %i[integer bigint float decimal].include?(@column.type)
102
+
103
+ v = @model_class.validators_on(@column.name).find do |x|
104
+ x.is_a?(ActiveModel::Validations::NumericalityValidator)
105
+ end
106
+ return nil unless v
107
+
108
+ opts = v.options
109
+ min = opts[:greater_than_or_equal_to] || (opts[:greater_than] && opts[:greater_than] + 1) || 1
110
+ max = opts[:less_than_or_equal_to] || (opts[:less_than] && opts[:less_than] - 1) || (min + 1000)
111
+
112
+ if @column.type == :integer || @column.type == :bigint
113
+ rand(min.to_i..max.to_i)
114
+ else
115
+ rand(min.to_f..max.to_f).round(2)
116
+ end
117
+ end
118
+
119
+ def name_based_value
120
+ case @column.name
121
+ when "email", /_email\z/ then ::Faker::Internet.unique.email
122
+ when "first_name" then ::Faker::Name.first_name
123
+ when "last_name" then ::Faker::Name.last_name
124
+ when "name", "full_name" then ::Faker::Name.name
125
+ when "username", "login", "handle" then ::Faker::Internet.unique.username
126
+ when "phone", "phone_number", /_phone\z/ then ::Faker::PhoneNumber.cell_phone
127
+ when "address", "street", "street_address" then ::Faker::Address.street_address
128
+ when "city" then ::Faker::Address.city
129
+ when "state", "province" then ::Faker::Address.state
130
+ when "country" then ::Faker::Address.country
131
+ when "zip", "zipcode", "postal_code" then ::Faker::Address.zip
132
+ when "url", "website", "homepage" then ::Faker::Internet.url
133
+ when "title" then ::Faker::Lorem.sentence(word_count: 4)
134
+ when "body", "content", "description", "bio",
135
+ "summary", "notes" then ::Faker::Lorem.paragraph(sentence_count: 3)
136
+ when "company", "company_name" then ::Faker::Company.name
137
+ when "slug" then ::Faker::Internet.unique.slug
138
+ when "uuid" then SecureRandom.uuid
139
+ when "birthdate", "birthday", "dob", "date_of_birth" then ::Faker::Date.birthday(min_age: 18, max_age: 80)
140
+ when "age" then rand(18..80)
141
+ when "color" then ::Faker::Color.color_name
142
+ when "latitude" then ::Faker::Address.latitude.to_f
143
+ when "longitude" then ::Faker::Address.longitude.to_f
144
+ when "price", "amount" then (rand * 1000).round(2)
145
+ when "quantity", "qty" then rand(1..100)
146
+ when /\Apassword(_digest)?\z/
147
+ # let model's has_secure_password handle this; assign a literal password
148
+ "password123"
149
+ end
150
+ end
151
+
152
+ def type_based_value
153
+ case @column.type
154
+ when :string then ::Faker::Lorem.word.capitalize
155
+ when :text then ::Faker::Lorem.paragraph(sentence_count: 3)
156
+ when :integer then rand(1..1000)
157
+ when :bigint then rand(1..1_000_000)
158
+ when :float, :decimal then (rand * 1000).round(2)
159
+ when :boolean then [true, false].sample
160
+ when :date then ::Faker::Date.between(from: Date.today - 365, to: Date.today)
161
+ when :datetime, :timestamp then ::Faker::Time.between(from: Time.now - (60 * 60 * 24 * 365), to: Time.now)
162
+ when :time then Time.now
163
+ when :json, :jsonb then {}
164
+ when :uuid then SecureRandom.uuid
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUiScaffold
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_ui_scaffold/version"
4
+ require "ruby_ui_scaffold/field_type_mapper"
5
+ require "ruby_ui_scaffold/attribute_helpers"
6
+ require "ruby_ui_scaffold/component_resolver"
7
+ require "ruby_ui_scaffold/component_installer"
8
+ require "ruby_ui_scaffold/value_generator"
9
+ require "ruby_ui_scaffold/seeder"
10
+
11
+ # Make sure transitive deps' Railties fire when the gem is loaded via
12
+ # Bundler.require. Without these, the helpers (lucide_icon, Faker) aren't
13
+ # registered in the host app, the `phlex:install` / `ruby_ui:component`
14
+ # generators aren't discoverable, and the generated views/seed command crash.
15
+ require "phlex-rails"
16
+ require "literal"
17
+ require "lucide-rails"
18
+
19
+ require "ruby_ui_scaffold/railtie" if defined?(Rails::Railtie)
20
+
21
+ module RubyUiScaffold
22
+ end