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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +71 -12
- data/lib/permittable/audit.rb +231 -20
- data/lib/permittable/generator.rb +47 -11
- data/lib/permittable/open_api.rb +429 -79
- data/lib/permittable/tasks/generate.rake +9 -6
- data/lib/permittable/version.rb +1 -1
- data/lib/permittable.rb +158 -40
- metadata +2 -2
|
@@ -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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|