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.



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

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.



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

def columns
  @columns
end

#skippedObject (readonly)

Returns the value of attribute skipped.



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

def skipped
  @skipped
end

#unmatchedObject (readonly)

Returns the value of attribute unmatched.



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

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.



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

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



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

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



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

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

Instance Method Details

#attribute_namesObject



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

def attribute_names = columns.map(&:name)

#belongs_tosObject



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

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

#booleansObject



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

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

#empty?Boolean

Returns:

  • (Boolean)


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

def empty? = columns.empty?

#filterableObject



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

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)


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

def introspected? = @introspected

#live_errorsObject



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

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.



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

def primary_boolean = booleans.first

#schema_ordered?Boolean

Returns:

  • (Boolean)


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

def schema_ordered? = @schema_ordered

#searchableObject



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

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

#searchable?Boolean

Returns:

  • (Boolean)


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

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.



511
512
513
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 511

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

#timestamps?Boolean

Returns:

  • (Boolean)


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

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.



530
531
532
533
# File 'lib/generators/hibiki/rails/scaffold_schema.rb', line 530

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