rails-api-docs 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.

Potentially problematic release.


This version of rails-api-docs might be problematic. Click here for more details.

@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails-api-docs"
5
+
6
+ module RailsApiDocs
7
+ module Generators
8
+ # The full implementation lives here. `UpdateGenerator` is a thin alias
9
+ # subclass that just overrides the namespace — see update_generator.rb.
10
+ # Both invocations run identical code; the distinction is semantic:
11
+ #
12
+ # rails g rails-api-docs:init # scaffold the YAML the first time
13
+ # rails g rails-api-docs:update # re-run to absorb new routes
14
+ #
15
+ # The generator self-detects whether the YAML already exists and either
16
+ # creates it fresh (init flow) or appends only new routes (update flow).
17
+ # Existing entries are never modified — safe to re-run any time.
18
+ class InitGenerator < ::Rails::Generators::Base
19
+ # Force the hyphenated namespace so the CLI is `rails g rails-api-docs:init`,
20
+ # matching the gem name (the module-derived default would be `rails_api_docs:init`).
21
+ namespace "rails-api-docs:init"
22
+
23
+ desc <<~DESC
24
+ Initialize config/rails-api-docs.yml from your Rails app's routes.
25
+
26
+ Scaffolds the full config with every discovered route. To absorb
27
+ new routes after editing your routes file later, run
28
+ `rails g rails-api-docs:update` — it appends new routes without
29
+ touching existing entries.
30
+ DESC
31
+
32
+ # `nil` default lets us distinguish "flag not passed" (fall back to
33
+ # global config) from explicit `--no-api-only` (override config).
34
+ class_option :api_only, type: :boolean, default: nil,
35
+ desc: "Only include routes whose controller is JSON-returning " \
36
+ "(ActionController::API descendant, or action has `render json:`)"
37
+
38
+ class_option :only_controllers, type: :array, default: nil,
39
+ desc: "Whitelist controllers (others are dropped). " \
40
+ "Accepts space-, comma-, or bracket-separated names. " \
41
+ "Bare names match across namespaces (e.g. 'users' matches " \
42
+ "both `users` and `api/v1/users`); paths with '/' are exact. " \
43
+ "Examples:\n" \
44
+ " --only-controllers users posts\n" \
45
+ " --only-controllers=users,posts,comments\n" \
46
+ " --only-controllers=[users,posts,comments]\n" \
47
+ " --only-controllers api/v1/users # namespaced exact"
48
+
49
+ class_option :verbose_yaml, type: :boolean, default: nil,
50
+ desc: "Emit every possible YAML key per endpoint and field " \
51
+ "(format, enum, default, min/max, pattern, read/write_only, " \
52
+ "nullable, response headers/schema, etc.) with defaults. " \
53
+ "Default (without flag) emits the commonly-edited subset only."
54
+
55
+ def create_or_update_config_file
56
+ routes = Inspectors::RouteInspector.new(
57
+ route_set: RailsApiDocs.configuration.route_source,
58
+ config: scoped_config
59
+ ).call
60
+ routes = filter_json_only(routes) if api_only?
61
+
62
+ inferrer = Inspectors::BodyInferrer.new(root: destination_root, verbose: verbose_yaml?)
63
+ generated = Config::Builder.new(routes: routes, body_inferrer: inferrer, verbose: verbose_yaml?).call
64
+
65
+ if File.exist?(absolute_path)
66
+ append_into_existing(generated)
67
+ else
68
+ create_fresh(generated)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def api_only?
75
+ options[:api_only].nil? ? RailsApiDocs.configuration.api_only : options[:api_only]
76
+ end
77
+
78
+ def verbose_yaml?
79
+ options[:verbose_yaml].nil? ? RailsApiDocs.configuration.verbose_yaml : options[:verbose_yaml]
80
+ end
81
+
82
+ def filter_json_only(routes)
83
+ detector = Inspectors::JsonRouteDetector.new(root: destination_root)
84
+ kept = routes.select { |r| detector.call(controller: r[:controller], action: r[:action]) }
85
+ skipped = routes.size - kept.size
86
+ say_status :filtered, "--api-only kept #{kept.size}/#{routes.size} routes (#{skipped} non-JSON skipped)", :cyan if skipped.positive?
87
+ kept
88
+ end
89
+
90
+ # Returns a per-run configuration: a dup of the global config with
91
+ # CLI-supplied options overlaid. Inspectors get this explicit config
92
+ # so we don't mutate global state.
93
+ def scoped_config
94
+ cfg = RailsApiDocs.configuration.dup
95
+ only_ctrls = parse_only_controllers
96
+ cfg.only_controllers = only_ctrls if only_ctrls
97
+ cfg
98
+ end
99
+
100
+ # Accepts all four CLI forms:
101
+ # --only-controllers users posts (space — Thor native array)
102
+ # --only-controllers=users,posts (comma)
103
+ # --only-controllers=[users,posts,comments] (bracketed)
104
+ # --only-controllers=[ users , posts ] (whitespace inside)
105
+ # Returns nil if flag wasn't passed, [] if passed but empty after parsing.
106
+ def parse_only_controllers
107
+ raw = options[:only_controllers]
108
+ return nil unless raw
109
+
110
+ raw.flat_map { |item| item.gsub(/[\[\]]/, "").split(",") }
111
+ .map(&:strip)
112
+ .reject(&:empty?)
113
+ end
114
+
115
+ def relative_path
116
+ RailsApiDocs.configuration.config_path
117
+ end
118
+
119
+ def absolute_path
120
+ File.join(destination_root, relative_path)
121
+ end
122
+
123
+ def create_fresh(generated)
124
+ content = file_header + YAML.dump(generated)
125
+ create_file relative_path, content
126
+ end
127
+
128
+ def append_into_existing(generated)
129
+ existing = Config::Loader.load(absolute_path)
130
+ appender = Config::Appender.new(existing: existing, generated: generated)
131
+
132
+ unless appender.changes?
133
+ say_status :unchanged, relative_path, :yellow
134
+ return
135
+ end
136
+
137
+ header = Config::Loader.header(absolute_path)
138
+ merged = appender.call
139
+ content = (header.empty? ? file_header : header) + YAML.dump(merged)
140
+
141
+ File.write(absolute_path, content)
142
+
143
+ d = appender.diff
144
+ new_sections = d[:new_sections].size
145
+ new_endpoints = d[:new_endpoints_by_section].values.sum(&:size)
146
+ say_status :updated, "#{relative_path} (+#{new_sections} section(s), +#{new_endpoints} endpoint(s))", :green
147
+ end
148
+
149
+ def file_header
150
+ <<~YAML
151
+ # config/rails-api-docs.yml
152
+ # Auto-generated by rails-api-docs.
153
+ #
154
+ # You can safely edit this file:
155
+ # - Add descriptions, examples, body fields and params.
156
+ # - Set `show: false` to hide a route or section from the docs.
157
+ # - Tweak `general_configurations` (colors, title, base_url, etc.).
158
+ #
159
+ # Re-run `rails g rails-api-docs:update` whenever your routes change —
160
+ # it only APPENDS new routes; existing entries are never modified.
161
+ # Delete a section/endpoint from this file to have it regenerated.
162
+
163
+ YAML
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../init/init_generator"
4
+
5
+ module RailsApiDocs
6
+ module Generators
7
+ # Alias for InitGenerator with a different namespace. Both commands run
8
+ # the same code; this one reads more naturally for re-runs:
9
+ #
10
+ # rails g rails-api-docs:update # equivalent to :init
11
+ # rails g rails-api-docs:update --api-only # all flags supported
12
+ #
13
+ # The generator's behavior is self-detecting (file exists → append-only
14
+ # merge; file missing → fresh scaffold), so the practical distinction is
15
+ # purely the name printed in `rails g --help`.
16
+ class UpdateGenerator < InitGenerator
17
+ namespace "rails-api-docs:update"
18
+
19
+ desc <<~DESC
20
+ Update config/rails-api-docs.yml with new routes from your Rails app.
21
+
22
+ Append-only: only routes not yet in the YAML are added — existing
23
+ entries (and your edits) are never modified. Accepts the same flags
24
+ as `rails g rails-api-docs:init`.
25
+ DESC
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiDocs
4
+ module Config
5
+ # Append-only merge between an existing parsed config and a freshly
6
+ # generated one.
7
+ #
8
+ # Rules:
9
+ # - Endpoint identity = "#{method} #{path}".
10
+ # - Existing endpoints win on every field (user edits are sacred).
11
+ # - New endpoints (not in existing) are appended to their section's
12
+ # `endpoints` array in the order returned by RouteInspector.
13
+ # - Brand-new sections are appended at the end of `sections`.
14
+ # - `general_configurations`: existing keys win; only missing keys
15
+ # are filled in from the generated defaults — so a new gem version
16
+ # introducing a new option doesn't force a manual edit.
17
+ class Appender
18
+ def initialize(existing:, generated:)
19
+ @existing = existing || {}
20
+ @generated = generated || {}
21
+ end
22
+
23
+ def call
24
+ {
25
+ "general_configurations" => merge_general,
26
+ "sections" => merge_sections
27
+ }
28
+ end
29
+
30
+ # { new_sections: [keys...], new_endpoints_by_section: { key => [endpoints...] } }
31
+ def diff
32
+ {
33
+ new_sections: new_section_keys,
34
+ new_endpoints_by_section: new_endpoints_by_section
35
+ }
36
+ end
37
+
38
+ def changes?
39
+ d = diff
40
+ !d[:new_sections].empty? || !d[:new_endpoints_by_section].empty?
41
+ end
42
+
43
+ private
44
+
45
+ def merge_general
46
+ existing = @existing["general_configurations"] || {}
47
+ generated = @generated["general_configurations"] || {}
48
+ generated.merge(existing)
49
+ end
50
+
51
+ def merge_sections
52
+ existing = @existing["sections"] || {}
53
+ generated = @generated["sections"] || {}
54
+
55
+ merged = existing.dup
56
+
57
+ generated.each do |key, gen_section|
58
+ merged[key] = merged.key?(key) ? merge_section(merged[key], gen_section) : gen_section
59
+ end
60
+
61
+ merged
62
+ end
63
+
64
+ def merge_section(existing_section, generated_section)
65
+ existing_endpoints = existing_section["endpoints"] || []
66
+ generated_endpoints = generated_section["endpoints"] || []
67
+
68
+ existing_keys = existing_endpoints.map { |e| endpoint_key(e) }
69
+ new_only = generated_endpoints.reject { |e| existing_keys.include?(endpoint_key(e)) }
70
+
71
+ existing_section.merge("endpoints" => existing_endpoints + new_only)
72
+ end
73
+
74
+ def endpoint_key(endpoint)
75
+ "#{endpoint['method']} #{endpoint['path']}"
76
+ end
77
+
78
+ def new_section_keys
79
+ existing_keys = (@existing["sections"] || {}).keys
80
+ generated_keys = (@generated["sections"] || {}).keys
81
+ generated_keys - existing_keys
82
+ end
83
+
84
+ def new_endpoints_by_section
85
+ result = {}
86
+ (@generated["sections"] || {}).each do |section_key, gen_section|
87
+ existing_section = @existing.dig("sections", section_key)
88
+ next unless existing_section
89
+
90
+ existing_keys = (existing_section["endpoints"] || []).map { |e| endpoint_key(e) }
91
+ new_endpoints = (gen_section["endpoints"] || []).reject { |e| existing_keys.include?(endpoint_key(e)) }
92
+ result[section_key] = new_endpoints unless new_endpoints.empty?
93
+ end
94
+ result
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "active_support/core_ext/string/inflections"
5
+
6
+ module RailsApiDocs
7
+ module Config
8
+ # Turns the array of route hashes produced by RouteInspector into the
9
+ # full config structure that gets written to config/rails-api-docs.yml.
10
+ #
11
+ # Output shape:
12
+ # {
13
+ # "general_configurations" => { ... defaults ... },
14
+ # "sections" => {
15
+ # "<controller_key>" => {
16
+ # "name" => "...",
17
+ # "description" => "",
18
+ # "show" => true,
19
+ # "endpoints" => [ { "method" =>, "path" =>, ... }, ... ]
20
+ # }
21
+ # }
22
+ # }
23
+ class Builder
24
+ DEFAULT_GENERAL = {
25
+ "title" => "API Documentation",
26
+ "base_url" => "https://api.example.com",
27
+ "primary_color" => "#CC0000",
28
+ "secondary_color" => "#2E2E2E",
29
+ "accent_color" => "#D30001",
30
+ "font_family" => "system-ui, -apple-system, sans-serif",
31
+ "show_curl" => true,
32
+ "show_examples" => true
33
+ }.freeze
34
+
35
+ ACTION_VERB_PHRASE = {
36
+ "index" => "List",
37
+ "show" => "Show",
38
+ "new" => "New",
39
+ "create" => "Create",
40
+ "edit" => "Edit",
41
+ "update" => "Update",
42
+ "destroy" => "Delete"
43
+ }.freeze
44
+
45
+ def initialize(routes:, general: nil, body_inferrer: nil, verbose: false)
46
+ @routes = routes
47
+ @general = general || DEFAULT_GENERAL.dup
48
+ @body_inferrer = body_inferrer
49
+ @verbose = verbose
50
+ end
51
+
52
+ def call
53
+ {
54
+ "general_configurations" => @general,
55
+ "sections" => build_sections
56
+ }
57
+ end
58
+
59
+ def to_yaml
60
+ YAML.dump(call)
61
+ end
62
+
63
+ private
64
+
65
+ def build_sections
66
+ @routes.group_by { |r| r[:controller] }.each_with_object({}) do |(controller, routes), acc|
67
+ acc[controller] = {
68
+ "name" => section_name(controller),
69
+ "description" => "",
70
+ "show" => true,
71
+ "endpoints" => routes.map { |r| build_endpoint(r) }
72
+ }
73
+ end
74
+ end
75
+
76
+ def section_name(controller)
77
+ controller.split("/").last.to_s.tr("_", " ").split.map(&:capitalize).join(" ")
78
+ end
79
+
80
+ def build_endpoint(route)
81
+ endpoint = {
82
+ "method" => route[:verb],
83
+ "path" => route[:path],
84
+ "name" => endpoint_name(route),
85
+ "description" => "",
86
+ "show" => true
87
+ }
88
+
89
+ endpoint.merge!(verbose_endpoint_meta) if @verbose
90
+
91
+ params = path_params(route)
92
+ endpoint["params"] = params unless params.empty?
93
+ endpoint["params"] = [] if @verbose && params.empty? # discoverability
94
+
95
+ body = @body_inferrer&.call(controller: route[:controller], action: route[:action])
96
+ endpoint["body"] = body if body && !body.empty?
97
+ endpoint["body"] = [] if @verbose && (body.nil? || body.empty?)
98
+
99
+ endpoint["request_example"] = "" if @verbose
100
+ endpoint["responses"] = response_stub
101
+
102
+ endpoint
103
+ end
104
+
105
+ def response_stub
106
+ if @verbose
107
+ { "200" => { "description" => "", "headers" => [], "schema" => [], "example" => "" } }
108
+ else
109
+ { "200" => { "description" => "", "example" => "" } }
110
+ end
111
+ end
112
+
113
+ # Methods (not frozen constants) so each endpoint gets fresh array
114
+ # values — otherwise YAML.dump emits noisy anchors tying multiple
115
+ # endpoints together.
116
+ def verbose_endpoint_meta
117
+ { "deprecated" => false, "auth" => "", "tags" => [], "headers" => [] }
118
+ end
119
+
120
+ def verbose_field_defaults
121
+ {
122
+ "format" => "",
123
+ "enum" => [],
124
+ "default" => nil,
125
+ "min" => nil,
126
+ "max" => nil,
127
+ "min_length" => nil,
128
+ "max_length" => nil,
129
+ "pattern" => "",
130
+ "read_only" => false,
131
+ "write_only" => false,
132
+ "nullable" => false
133
+ }
134
+ end
135
+
136
+ def path_params(route)
137
+ Array(route[:path_params]).map do |name|
138
+ type = path_param_type(name.to_s)
139
+ base = {
140
+ "name" => name.to_s,
141
+ "type" => type,
142
+ "required" => true,
143
+ "in" => "path",
144
+ "description" => "",
145
+ "example" => RailsApiDocs::SampleValue.for(type)
146
+ }
147
+ @verbose ? base.merge(verbose_field_defaults) : base
148
+ end
149
+ end
150
+
151
+ def path_param_type(name)
152
+ name == "id" || name.end_with?("_id") ? "integer" : "string"
153
+ end
154
+
155
+ def endpoint_name(route)
156
+ phrase = ACTION_VERB_PHRASE[route[:action]] || humanize(route[:action])
157
+ noun = route[:action] == "index" ? pluralize_last_segment(route[:controller])
158
+ : singularize_last_segment(route[:controller])
159
+ "#{phrase} #{noun.capitalize}"
160
+ end
161
+
162
+ def humanize(action)
163
+ action.to_s.tr("_", " ").capitalize
164
+ end
165
+
166
+ def singularize_last_segment(controller)
167
+ controller.split("/").last.to_s.singularize
168
+ end
169
+
170
+ def pluralize_last_segment(controller)
171
+ controller.split("/").last.to_s.pluralize
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RailsApiDocs
6
+ module Config
7
+ module Loader
8
+ module_function
9
+
10
+ def load(path)
11
+ return {} unless File.exist?(path)
12
+
13
+ raw = File.read(path)
14
+ YAML.safe_load(raw, permitted_classes: [Symbol, Date, Time]) || {}
15
+ end
16
+
17
+ # Returns the leading comment block ("#"-prefixed lines and blank lines
18
+ # at the top of the file). Used to preserve the auto-generated header
19
+ # — and any user notes they tacked at the top — across re-runs.
20
+ def header(path)
21
+ return "" unless File.exist?(path)
22
+
23
+ File.read(path)
24
+ .lines
25
+ .take_while { |line| line.start_with?("#") || line.strip.empty? }
26
+ .join
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiDocs
4
+ class Configuration
5
+ attr_accessor :config_path, :output_path, :mount_path, :mount_in_development,
6
+ :ignored_path_prefixes, :ignored_controllers, :ignored_actions,
7
+ :only_controllers, :api_only, :verbose_yaml
8
+ attr_writer :route_source
9
+
10
+ def initialize
11
+ @config_path = "config/rails-api-docs.yml"
12
+ @output_path = "public/api-docs.html"
13
+ @mount_path = "/rails/api-docs"
14
+ @mount_in_development = true
15
+ @route_source = nil
16
+
17
+ # User-extensible filters applied by RouteInspector. They stack on top
18
+ # of the built-in INTERNAL_PREFIXES list — they don't replace it.
19
+ #
20
+ # ignored_* are blacklists. only_controllers is a whitelist. Strings
21
+ # without "/" match by boundary-aware suffix (so "users" matches
22
+ # `users` and `api/v1/users` but not `super_users`). Strings with "/"
23
+ # match exactly. Regexps match via `Regexp#match?`.
24
+ # Blacklist wins when a controller is in both lists.
25
+ @ignored_path_prefixes = []
26
+ @ignored_controllers = []
27
+ @ignored_actions = []
28
+ @only_controllers = []
29
+
30
+ # When true, scaffold only includes routes whose controller is JSON-returning
31
+ # (inherits from ActionController::API, or has `render json:` in the action body).
32
+ # Can be set per-run via `rails g rails-api-docs:init --api-only` (or `:update`).
33
+ @api_only = false
34
+
35
+ # When true, scaffold emits EVERY possible YAML key per endpoint and field
36
+ # (format, enum, default, min/max, pattern, read/write_only, nullable,
37
+ # response headers/schema, etc.) with defaults — full discoverability at
38
+ # the cost of much longer files. Default (false) emits only the commonly-
39
+ # edited keys: description, example, and the existing inferred fields.
40
+ # Per-run via `--verbose-yaml`.
41
+ @verbose_yaml = false
42
+ end
43
+
44
+ # Default lazy lookup — resolved at call time so Rails.application is set
45
+ # by the time the generator runs. Tests can inject a custom RouteSet.
46
+ def route_source
47
+ @route_source || Rails.application.routes
48
+ end
49
+ end
50
+
51
+ class << self
52
+ def configuration
53
+ @configuration ||= Configuration.new
54
+ end
55
+
56
+ def configure
57
+ yield configuration
58
+ end
59
+
60
+ def reset_configuration!
61
+ @configuration = Configuration.new
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module RailsApiDocs
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace RailsApiDocs
8
+
9
+ rake_tasks do
10
+ load File.expand_path("tasks/rails-api-docs.rake", __dir__)
11
+ end
12
+
13
+ generators do
14
+ require_relative "../generators/rails-api-docs/init/init_generator"
15
+ require_relative "../generators/rails-api-docs/update/update_generator"
16
+ end
17
+
18
+ initializer "rails_api_docs.mount" do |app|
19
+ config = RailsApiDocs.configuration
20
+ if config.mount_in_development && Rails.env.development?
21
+ app.routes.append do
22
+ mount RailsApiDocs::Engine, at: config.mount_path, as: :rails_api_docs
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiDocs
4
+ module Inspectors
5
+ # Composes ControllerInspector + SchemaInspector to produce a typed body
6
+ # field list for write actions (create / update).
7
+ #
8
+ # Returned shape per call:
9
+ #
10
+ # [
11
+ # { "name" => "email", "type" => "string", "required" => true },
12
+ # { "name" => "age", "type" => "integer", "required" => false }
13
+ # ]
14
+ #
15
+ # Returns `nil` (not an empty array) when there's nothing useful to add —
16
+ # the Builder uses that to decide whether to emit a `body:` key at all.
17
+ class BodyInferrer
18
+ WRITE_ACTIONS = %w[create update].freeze
19
+
20
+ def initialize(root: nil, controller_inspector: nil, schema_inspector: nil, verbose: false)
21
+ @root = root
22
+ @controller_inspector_class = controller_inspector || ControllerInspector
23
+ @schema_inspector_class = schema_inspector || SchemaInspector
24
+ @verbose = verbose
25
+ @cache = {}
26
+ end
27
+
28
+ def call(controller:, action:)
29
+ return nil unless WRITE_ACTIONS.include?(action.to_s)
30
+
31
+ permitted = controller_permits(controller)
32
+ return nil if permitted.empty?
33
+
34
+ types = column_types(controller)
35
+
36
+ permitted.map { |name| build_field(name, types[name]) }
37
+ end
38
+
39
+ private
40
+
41
+ def build_field(name, col)
42
+ type = col ? col[:type].to_s : "string"
43
+ required = col ? required?(col) : false
44
+ base = {
45
+ "name" => name,
46
+ "type" => type,
47
+ "required" => required,
48
+ "description" => "",
49
+ "example" => RailsApiDocs::SampleValue.for(type)
50
+ }
51
+ @verbose ? base.merge(verbose_field_defaults) : base
52
+ end
53
+
54
+ # Returned as a fresh hash (and fresh array/value instances inside) so
55
+ # multiple fields don't share references — otherwise `YAML.dump` emits
56
+ # noisy anchors (`enum: &1 []` / `enum: *1`) tying them together.
57
+ def verbose_field_defaults
58
+ {
59
+ "format" => "",
60
+ "enum" => [],
61
+ "default" => nil,
62
+ "min" => nil,
63
+ "max" => nil,
64
+ "min_length" => nil,
65
+ "max_length" => nil,
66
+ "pattern" => "",
67
+ "read_only" => false,
68
+ "write_only" => false,
69
+ "nullable" => false
70
+ }
71
+ end
72
+
73
+ def required?(col)
74
+ !col[:null] && col[:default].nil?
75
+ end
76
+
77
+ def controller_permits(controller)
78
+ @cache[[:permits, controller]] ||=
79
+ @controller_inspector_class.new(controller: controller, root: @root).call
80
+ end
81
+
82
+ def column_types(controller)
83
+ @cache[[:schema, controller]] ||=
84
+ @schema_inspector_class.new(controller: controller).call
85
+ end
86
+ end
87
+ end
88
+ end