Class: Hibiki::Rails::Generators::ScaffoldSchema

Inherits:
Object
  • Object
show all
Defined in:
lib/generators/hibiki/rails/scaffold_schema.rb

Overview

The column list a scaffold generator hands its templates, plus every list derived from it.

Two builders, because a scaffold has two genuinely different sources of truth and neither covers both commands:

from_attributes  `hibiki:rails:scaffold Book title:string` — the
               migration has NOT run when the controller generator
               is invoked, so the schema does not exist yet.
from_model       `hibiki:rails:scaffold_controller Book` with no
               field list — the only source is the real schema.

Explicit attributes always win, matching Rails' own precedence.

But ORDER and FACTS are separate questions, and only order has to come from the arguments. from_attributes takes a model: for the case where both sources exist — scaffold_controller Book title:string ... against a migrated app, which is exactly what the field-order notice recommends — and reads every fact from the schema while the argument list decides the sequence. Without it, taking that advice silently cost the validators behind live_errors, a number field's min/max, and the label column a belongs_to's select displays.

Constant Summary collapse

TIMESTAMPS =
%w[created_at updated_at].freeze
REFUSALS =

Attribute shapes that cannot be a ReactiveForm signal, a Data member or a collection_select. They are refused out loud rather than emitted half-working: a silently dropped column is the §10 failure class, where everything looks generated and one field is quietly missing.

{
  rich_text?: "rich text needs its own editor and cannot be a form signal",
  attachment?: "Active Storage attachments are not form signals",
  attachments?: "Active Storage attachments are not form signals",
  password_digest?: "a password digest must not travel over a channel",
  token?: "a token must not travel over a channel",
  polymorphic?: "a polymorphic belongs_to has no single collection to select from"
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(columns:, skipped: [], unmatched: [], introspection: nil, timestamps: true, introspected: false, schema_ordered: false) ⇒ ScaffoldSchema

Returns a new instance of ScaffoldSchema.



477
478
479
480
481
482
483
484
485
486
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 477

def initialize(columns:, skipped: [], unmatched: [], introspection: nil,
               timestamps: true, introspected: false, schema_ordered: false)
  @columns = columns
  @skipped = skipped
  @unmatched = unmatched
  @introspection = introspection
  @timestamps = timestamps
  @introspected = introspected
  @schema_ordered = schema_ordered
end

Instance Attribute Details

#columnsObject (readonly)

Returns the value of attribute columns.



404
405
406
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 404

def columns
  @columns
end

#skippedObject (readonly)

Returns the value of attribute skipped.



404
405
406
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 404

def skipped
  @skipped
end

#unmatchedObject (readonly)

Returns the value of attribute unmatched.



404
405
406
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 404

def unmatched
  @unmatched
end

Class Method Details

.from_attributes(attributes, timestamps: true, model: nil) ⇒ Object

model is nil whenever there is nothing to read: no constant, or no table yet — which is EVERY hibiki:rails:scaffold run, since it invokes the controller generator before its own migration. That case is this method's original behaviour, unchanged.



413
414
415
416
417
418
419
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 413

def from_attributes(attributes, timestamps: true, model: nil)
  introspection = ModelIntrospection.new(model) if model
  supported, refused = attributes.partition { refusal_for(it).nil? }
  return from_arguments_only(supported, refused, timestamps) unless introspection

  merge(supported, refused, introspection)
end

.from_model(model) ⇒ Object



421
422
423
424
425
426
427
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 421

def from_model(model)
  introspection = ModelIntrospection.new(model)

  new(columns: introspection.columns, skipped: introspection.skipped,
      introspection: introspection, timestamps: introspection.timestamps?,
      introspected: true, schema_ordered: true)
end

.refusal_for(attr) ⇒ Object



407
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 407

def refusal_for(attr) = REFUSALS.find { |query, _| attr.public_send(query) }&.last

Instance Method Details

#attribute_namesObject



501
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 501

def attribute_names = columns.map(&:name)

#belongs_tosObject



499
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 499

def belongs_tos = columns.select(&:belongs_to?)

#booleansObject



500
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 500

def booleans = columns.select(&:filterable?)

#empty?Boolean

Returns:

  • (Boolean)


495
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 495

def empty? = columns.empty?

#filterableObject



498
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 498

def filterable = columns.select(&:filterable?).map(&:name)

#introspected?Boolean

Two independent facts, and the merge is the case that separates them: introspected? says the FACTS came from a live model, while schema_ordered? says the schema also decided the ORDER — which is the only thing the field-order notice has anything to say about.

Returns:

  • (Boolean)


492
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 492

def introspected? = @introspected

#live_errorsObject



524
525
526
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 524

def live_errors
  columns.filter_map { |column| [column.name, column.live_error_clause] if column.live_error_clause }
end

#primary_booleanObject

One grouped COUNT covers both numbers, so the counts sentence gets a boolean breakdown for free. With no boolean column there is nothing to group by and the sentence loses its trailing clause.



516
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 516

def primary_boolean = booleans.first

#row_membersObject

A belongs_to contributes two members: the foreign key the form binds to, and the label the row partial prints.



520
521
522
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 520

def row_members
  [:id, *columns.flat_map { it.belongs_to? ? [it.name, it.display_reader] : [it.name] }]
end

#schema_ordered?Boolean

Returns:

  • (Boolean)


493
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 493

def schema_ordered? = @schema_ordered

#searchableObject



497
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 497

def searchable = columns.select(&:searchable?).map(&:name)

#searchable?Boolean

Returns:

  • (Boolean)


502
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 502

def searchable? = searchable.any?

#sortableObject

:id first because it is the default sort and always exists; :created_at last because "newest" is the sort a reader reaches for that no declared column provides. :updated_at is deliberately absent — it moves on every write, so ordering by it makes the list reshuffle under the reader.



509
510
511
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 509

def sortable
  [:id, *columns.select(&:sortable?).map(&:name), *(:created_at if timestamps?)]
end

#timestamps?Boolean

Returns:

  • (Boolean)


494
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 494

def timestamps? = @timestamps

#unmirrored_uniquenessObject

§5.15: unique indexes with no validator mirroring them, as column-name lists. The model is authoritative wherever there is one — it knows composite indexes, which no argument list can express — and title:string:uniq is the fallback for the path with no model to ask. Read off the attribute rather than the column for the same reason refusal_for is: it is a question about what was TYPED.



534
535
536
537
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 534

def unmirrored_uniqueness
  @unmirrored_uniqueness ||= @introspection&.unmirrored_unique_indexes(columns.map(&:name)) ||
                             columns.select { it.attr.has_uniq_index? }.map { [it.name] }
end