permittable 0.10.0 → 0.11.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.
@@ -297,33 +297,59 @@ module Permittable
297
297
  # nil, until one drafts or none is left: a draft rooted at the other
298
298
  # envelope in the file is a better starting point than no draft, and it
299
299
  # is what master drafted.
300
- def for_controller(controller, source: nil, model: nil)
300
+ #
301
+ # `except:` names actions of DEFAULT_ACTIONS to leave out of the draft —
302
+ # the ones Permittable.audit_ignore names (see targets). With none left
303
+ # there is nothing to draft.
304
+ def for_controller(controller, source: nil, model: nil, except: [])
305
+ return nil if (DEFAULT_ACTIONS - except.map(&:to_sym)).empty?
306
+
301
307
  model ||= infer_model(controller)
302
308
  excluded = []
303
309
  loop do
304
310
  scan = scan(source, model: model, exclude: excluded)
305
- result = draft(model: model, scan: scan)
311
+ result = draft(model: model, scan: scan, except: except)
306
312
  return result if result || !scan.found? || excluded.include?(scan.root)
307
313
 
308
314
  excluded << scan.root
309
315
  end
310
316
  end
311
317
 
318
+ # What `permittable:generate` drafts when no controller is named, as
319
+ # { controller => actions to leave out of its draft }: every controller
320
+ # with no contract yet, less what the audit ignores (Audit.ignore_reason).
321
+ # ActiveStorage's direct uploads controller is the case this exists for —
322
+ # it was drafted, and a contract pasted into a gem's controller is not
323
+ # one the app can keep. A "controller#action" entry takes only that
324
+ # action out of the draft, and a controller with no drafted action left
325
+ # is skipped. Naming a controller to the task drafts it regardless, the
326
+ # way it drafts a covered one.
327
+ def targets(controllers, ignore: Permittable.audit_ignore, root: Audit.app_root)
328
+ controllers.each_with_object({}) do |controller, found|
329
+ next if controller.respond_to?(:permittable_contracts) && controller.permittable_contracts.any?
330
+
331
+ skipped = DEFAULT_ACTIONS.select { |action| Audit.ignore_reason(controller, action, ignore: ignore, root: root) }
332
+ found[controller] = skipped unless skipped == DEFAULT_ACTIONS
333
+ end
334
+ end
335
+
312
336
  # The core: knowledge in (columns and/or a scan), snippet out. Returns a
313
337
  # String of valid Ruby, or nil when neither source of knowledge exists —
314
338
  # or when neither can declare a single field, since a contract of only
315
339
  # TODO lines raises `a contract must declare at least one field` the
316
340
  # moment it is pasted.
317
- def draft(model: nil, scan: nil)
341
+ def draft(model: nil, scan: nil, except: [])
318
342
  columns = columns_for(model)
319
343
  scan = nil unless scan&.found?
320
344
  return nil unless columns || scan
321
- return column_draft(model, columns.values, []) unless scan
345
+ return column_draft(model, columns.values, [], except: except) unless scan
322
346
 
323
347
  shared = scanned_lines(scan, listed_columns(columns, :shared))
324
- return fallback_draft(model, scan, columns) unless declares_field?(shared)
348
+ return fallback_draft(model, scan, columns, except: except) unless declares_field?(shared)
325
349
 
326
- render(root: scan.root, model: columns && model) { |rule| scanned_lines(scan, listed_columns(columns, rule)) }
350
+ render(root: scan.root, model: columns && model, except: except) do |rule|
351
+ scanned_lines(scan, listed_columns(columns, rule))
352
+ end
327
353
  end
328
354
 
329
355
  # The columns as one rule drafts them (see DraftColumn#in_rule), each
@@ -356,7 +382,7 @@ module Permittable
356
382
  # `permit(policy(@post).permitted_attributes)` the scanner skips — says
357
383
  # nothing about the body, so it gets the model's root, as master drafted.
358
384
  # With no columns either there is nothing loadable to draft, so nil.
359
- def fallback_draft(model, scan, columns)
385
+ def fallback_draft(model, scan, columns, except: [])
360
386
  return nil unless columns
361
387
 
362
388
  root = scan.root || (rootless_body?(scan) ? nil : default_root(model))
@@ -365,7 +391,7 @@ module Permittable
365
391
  # As in drop_drafted_todos: a rootless key the columns now declare is
366
392
  # not also a "not in this contract" TODO.
367
393
  scan = scan.dup.tap { |copy| copy.rootless = Array(scan.rootless).reject { |arg| drafted.key?(scalar_key(arg).to_s) } }
368
- column_draft(model, drafted.values, scan_todo_lines(scan), root: root)
394
+ column_draft(model, drafted.values, scan_todo_lines(scan), root: root, except: except)
369
395
  end
370
396
 
371
397
  # Whether a rootless scan's calls carried at least one body field —
@@ -377,8 +403,10 @@ module Permittable
377
403
 
378
404
  # A draft from the columns, or nil when none of them has a contract type
379
405
  # to declare it with (render returns nil for a body of comments only).
380
- def column_draft(model, columns, todos, root: default_root(model))
381
- render(root: root, model: model) { |rule| column_lines(columns.map { |column| column.in_rule(rule) }) + todos }
406
+ def column_draft(model, columns, todos, root: default_root(model), except: [])
407
+ render(root: root, model: model, except: except) do |rule|
408
+ column_lines(columns.map { |column| column.in_rule(rule) }) + todos
409
+ end
382
410
  end
383
411
 
384
412
  def infer_model(controller)
@@ -1005,12 +1033,20 @@ module Permittable
1005
1033
  # no field, which raises `a contract must declare at least one field`
1006
1034
  # when pasted. There is then nothing loadable to draft: nil, as for no
1007
1035
  # knowledge at all, which the rake task already skips.
1008
- def render(root:, model:, &lines)
1036
+ #
1037
+ # An action in `except` is dropped from the rule that declares it, and a
1038
+ # rule left with no action is dropped: excepting :create from a split
1039
+ # draft leaves the :update rule as drafted, with nothing required.
1040
+ def render(root:, model:, except: [], &lines)
1009
1041
  shared = lines.call(:shared)
1010
1042
  return nil if shared.all? { |line| line.start_with?("#") }
1011
1043
 
1012
1044
  update = lines.call(:update)
1013
1045
  rules = shared == update ? [[DEFAULT_ACTIONS, shared]] : [[%i[create], lines.call(:create)], [%i[update], update]]
1046
+ skipped = except.map(&:to_sym)
1047
+ rules = rules.filter_map { |actions, body| [actions - skipped, body] unless (actions - skipped).empty? }
1048
+ return nil if rules.empty?
1049
+
1014
1050
  bodies = rules.map do |actions, body|
1015
1051
  "#{signature(root: root, model: model, actions: actions)}\n#{body.map { |line| " #{line}\n" }.join}end\n"
1016
1052
  end