rails_map 1.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.
- checksums.yaml +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/material_theme_project_new.xml +12 -0
- data/AUTHENTICATION.md +221 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/QUICKSTART.md +156 -0
- data/README.md +178 -0
- data/Rakefile +8 -0
- data/app/controllers/rails_map/docs_controller.rb +73 -0
- data/app/models/rails_map/user.rb +12 -0
- data/app/views/rails_map/docs/_styles.html.erb +489 -0
- data/app/views/rails_map/docs/controller.html.erb +137 -0
- data/app/views/rails_map/docs/index.html.erb +212 -0
- data/app/views/rails_map/docs/model.html.erb +214 -0
- data/app/views/rails_map/docs/routes.html.erb +139 -0
- data/config/initializers/rails_map.example.rb +44 -0
- data/config/routes.rb +9 -0
- data/docs/index.html +1354 -0
- data/lib/generators/rails_map/install_generator.rb +49 -0
- data/lib/generators/rails_map/templates/README +47 -0
- data/lib/generators/rails_map/templates/initializer.rb +38 -0
- data/lib/generators/rails_map/templates/migration.rb +14 -0
- data/lib/rails_map/auth.rb +15 -0
- data/lib/rails_map/configuration.rb +35 -0
- data/lib/rails_map/engine.rb +11 -0
- data/lib/rails_map/generators/html_generator.rb +120 -0
- data/lib/rails_map/parsers/model_parser.rb +257 -0
- data/lib/rails_map/parsers/route_parser.rb +356 -0
- data/lib/rails_map/railtie.rb +46 -0
- data/lib/rails_map/version.rb +5 -0
- data/lib/rails_map.rb +66 -0
- data/templates/controller.html.erb +74 -0
- data/templates/index.html.erb +64 -0
- data/templates/layout.html.erb +289 -0
- data/templates/model.html.erb +219 -0
- data/templates/routes.html.erb +86 -0
- metadata +144 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
|
|
6
|
+
module RailsMap
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Installs RailsMap with environment-based authentication (use --skip-auth to disable)"
|
|
14
|
+
|
|
15
|
+
class_option :skip_auth, type: :boolean, default: false, desc: "Skip authentication setup"
|
|
16
|
+
class_option :skip_routes, type: :boolean, default: false, desc: "Skip adding route mount"
|
|
17
|
+
|
|
18
|
+
def copy_initializer
|
|
19
|
+
template 'initializer.rb', 'config/initializers/rails_map.rb'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_route_mount
|
|
23
|
+
unless options[:skip_routes]
|
|
24
|
+
route "mount RailsMap::Engine, at: '/api-doc'"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_to_gitignore
|
|
29
|
+
gitignore_path = '.gitignore'
|
|
30
|
+
|
|
31
|
+
if File.exist?(gitignore_path)
|
|
32
|
+
gitignore_content = File.read(gitignore_path)
|
|
33
|
+
|
|
34
|
+
unless gitignore_content.include?('doc/api')
|
|
35
|
+
append_to_file gitignore_path do
|
|
36
|
+
"\n# Ignore generated documentation\n/doc/api\n"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
create_file gitignore_path, "# Ignore generated documentation\n/doc/api\n"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def show_readme
|
|
45
|
+
readme 'README' if behavior == :invoke
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
RailsMap has been installed!
|
|
4
|
+
|
|
5
|
+
✓ Configuration file created at:
|
|
6
|
+
config/initializers/rails_map.rb
|
|
7
|
+
|
|
8
|
+
<% unless options[:skip_routes] %>
|
|
9
|
+
✓ Route mounted in config/routes.rb:
|
|
10
|
+
mount RailsMap::Engine, at: '/api-doc'
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
✓ Added /doc/api to .gitignore
|
|
14
|
+
|
|
15
|
+
<% unless options[:skip_auth] %>
|
|
16
|
+
Authentication has been ENABLED (default)
|
|
17
|
+
|
|
18
|
+
Next steps:
|
|
19
|
+
1. Set environment variables:
|
|
20
|
+
export RAILS_MAP_USERNAME=admin
|
|
21
|
+
export RAILS_MAP_PASSWORD=your_secure_password
|
|
22
|
+
|
|
23
|
+
Or add to .env file:
|
|
24
|
+
RAILS_MAP_USERNAME=admin
|
|
25
|
+
RAILS_MAP_PASSWORD=your_secure_password
|
|
26
|
+
|
|
27
|
+
2. Restart your server and visit /api-doc
|
|
28
|
+
You'll be prompted to login with your credentials
|
|
29
|
+
|
|
30
|
+
To disable authentication, run:
|
|
31
|
+
rails destroy rails_map:install
|
|
32
|
+
rails g rails_map:install --skip-auth
|
|
33
|
+
<% else %>
|
|
34
|
+
Authentication has been DISABLED (--skip-auth flag used)
|
|
35
|
+
|
|
36
|
+
To enable authentication:
|
|
37
|
+
1. Set RAILS_MAP_USERNAME and RAILS_MAP_PASSWORD environment variables
|
|
38
|
+
2. Uncomment authentication in config/initializers/rails_map.rb
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
Documentation:
|
|
42
|
+
Visit http://localhost:3000/api-doc after starting your server
|
|
43
|
+
|
|
44
|
+
For more information, see:
|
|
45
|
+
https://rails-map.netlify.app
|
|
46
|
+
|
|
47
|
+
===============================================================================
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RailsMap.configure do |config|
|
|
4
|
+
# Application name displayed in the documentation
|
|
5
|
+
config.app_name = '<%= Rails.application.class.module_parent_name %>'
|
|
6
|
+
|
|
7
|
+
# Theme color (any valid CSS color)
|
|
8
|
+
config.theme_color = '#3B82F6'
|
|
9
|
+
|
|
10
|
+
# Output directory for static HTML generation
|
|
11
|
+
config.output_dir = Rails.root.join('doc', 'api').to_s
|
|
12
|
+
|
|
13
|
+
# Include timestamp columns (created_at, updated_at) in model documentation
|
|
14
|
+
config.include_timestamps = true
|
|
15
|
+
|
|
16
|
+
# Include model validations in documentation
|
|
17
|
+
config.include_validations = true
|
|
18
|
+
|
|
19
|
+
# Include model scopes in documentation
|
|
20
|
+
config.include_scopes = true
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# AUTHENTICATION
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# Set RAILS_MAP_USERNAME and RAILS_MAP_PASSWORD environment variables
|
|
26
|
+
# to enable authentication. Leave them unset for public access.
|
|
27
|
+
|
|
28
|
+
<% unless options[:skip_auth] %>
|
|
29
|
+
config.authenticate_with = proc {
|
|
30
|
+
RailsMap::Auth.authenticate(self)
|
|
31
|
+
}
|
|
32
|
+
<% else %>
|
|
33
|
+
# Authentication disabled via --skip-auth flag
|
|
34
|
+
# To enable: Set RAILS_MAP_USERNAME and RAILS_MAP_PASSWORD environment variables
|
|
35
|
+
# and uncomment the line below:
|
|
36
|
+
# config.authenticate_with = proc { RailsMap::Auth.authenticate(self) }
|
|
37
|
+
<% end %>
|
|
38
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsMapUsers < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_map_users do |t|
|
|
6
|
+
t.string :username, null: false
|
|
7
|
+
t.string :password_digest, null: false
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :rails_map_users, :username, unique: true
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMap
|
|
4
|
+
module Auth
|
|
5
|
+
class << self
|
|
6
|
+
# Authenticates using environment variables with HTTP Basic Auth
|
|
7
|
+
def authenticate(controller)
|
|
8
|
+
controller.authenticate_or_request_with_http_basic('Documentation') do |username, password|
|
|
9
|
+
username == (ENV['RAILS_MAP_USERNAME'] || 'admin') &&
|
|
10
|
+
password == (ENV['RAILS_MAP_PASSWORD'] || 'password')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMap
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :output_dir, :app_name, :include_timestamps, :include_validations,
|
|
6
|
+
:include_scopes, :theme_color, :authenticate_with
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@output_dir = default_output_dir
|
|
10
|
+
@app_name = default_app_name
|
|
11
|
+
@include_timestamps = true
|
|
12
|
+
@include_validations = true
|
|
13
|
+
@include_scopes = true
|
|
14
|
+
@theme_color = "#3B82F6" # Default blue
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def default_output_dir
|
|
20
|
+
if defined?(Rails)
|
|
21
|
+
Rails.root.join("doc", "api").to_s
|
|
22
|
+
else
|
|
23
|
+
File.join(Dir.pwd, "doc", "api")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def default_app_name
|
|
28
|
+
if defined?(Rails)
|
|
29
|
+
Rails.application.class.module_parent_name
|
|
30
|
+
else
|
|
31
|
+
"Rails Application"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RailsMap
|
|
7
|
+
module Generators
|
|
8
|
+
class HtmlGenerator
|
|
9
|
+
attr_reader :routes, :models, :output_dir
|
|
10
|
+
|
|
11
|
+
TEMPLATES_DIR = File.expand_path("../../../templates", __dir__)
|
|
12
|
+
|
|
13
|
+
def initialize(routes:, models:, output_dir:)
|
|
14
|
+
@routes = routes
|
|
15
|
+
@models = models
|
|
16
|
+
@output_dir = output_dir
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_all
|
|
20
|
+
generate_index
|
|
21
|
+
generate_routes_page
|
|
22
|
+
generate_controller_pages
|
|
23
|
+
generate_model_pages
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def generate_index
|
|
29
|
+
content = render_template("index", binding_for_index)
|
|
30
|
+
write_page("index.html", "Home", content)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def generate_routes_page
|
|
34
|
+
content = render_template("routes", binding_for_routes)
|
|
35
|
+
write_page("routes.html", "All Routes", content)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_controller_pages
|
|
39
|
+
routes.each do |controller_name, data|
|
|
40
|
+
content = render_template("controller", binding_for_controller(controller_name, data))
|
|
41
|
+
filename = "controllers/#{controller_name.gsub('/', '_')}.html"
|
|
42
|
+
write_page(filename, "#{controller_name.camelize}Controller", content)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate_model_pages
|
|
47
|
+
models.each do |name, model|
|
|
48
|
+
content = render_template("model", binding_for_model(model))
|
|
49
|
+
filename = "models/#{name.underscore.gsub('/', '_')}.html"
|
|
50
|
+
write_page(filename, name, content)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_template(name, template_binding)
|
|
55
|
+
template_path = File.join(TEMPLATES_DIR, "#{name}.html.erb")
|
|
56
|
+
template = ERB.new(File.read(template_path), trim_mode: "-")
|
|
57
|
+
template.result(template_binding)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write_page(filename, title, content)
|
|
61
|
+
layout_path = File.join(TEMPLATES_DIR, "layout.html.erb")
|
|
62
|
+
layout = ERB.new(File.read(layout_path), trim_mode: "-")
|
|
63
|
+
|
|
64
|
+
html = layout.result(binding_for_layout(title, content))
|
|
65
|
+
|
|
66
|
+
filepath = File.join(output_dir, filename)
|
|
67
|
+
FileUtils.mkdir_p(File.dirname(filepath))
|
|
68
|
+
File.write(filepath, html)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def binding_for_layout(title, content)
|
|
72
|
+
app_name = RailsMap.configuration.app_name
|
|
73
|
+
theme_color = RailsMap.configuration.theme_color
|
|
74
|
+
binding
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def binding_for_index
|
|
78
|
+
controllers = routes
|
|
79
|
+
controllers_count = routes.size
|
|
80
|
+
routes_count = routes.values.sum { |d| d[:routes].size }
|
|
81
|
+
models_count = models.size
|
|
82
|
+
binding
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def binding_for_routes
|
|
86
|
+
routes_count = routes.values.sum { |d| d[:routes].size }
|
|
87
|
+
binding
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def binding_for_controller(controller_name, data)
|
|
91
|
+
routes_list = data[:routes]
|
|
92
|
+
actions = data[:actions]
|
|
93
|
+
base_path = data[:base_path]
|
|
94
|
+
routes = routes_list # alias for template
|
|
95
|
+
binding
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def binding_for_model(model)
|
|
99
|
+
binding
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# String extensions for camelization if not in Rails
|
|
106
|
+
unless String.method_defined?(:camelize)
|
|
107
|
+
class String
|
|
108
|
+
def camelize
|
|
109
|
+
split(/[_\/]/).map(&:capitalize).join("::")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def underscore
|
|
113
|
+
gsub("::", "/")
|
|
114
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
115
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
116
|
+
.tr("-", "_")
|
|
117
|
+
.downcase
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsMap
|
|
4
|
+
module Parsers
|
|
5
|
+
class ModelParser
|
|
6
|
+
ColumnInfo = Struct.new(
|
|
7
|
+
:name, :type, :default, :null, :limit, :precision, :scale,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
AssociationInfo = Struct.new(
|
|
12
|
+
:name, :type, :class_name, :foreign_key, :options,
|
|
13
|
+
keyword_init: true
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
ValidationInfo = Struct.new(
|
|
17
|
+
:attribute, :kind, :options,
|
|
18
|
+
keyword_init: true
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
ScopeInfo = Struct.new(
|
|
22
|
+
:name, :arity,
|
|
23
|
+
keyword_init: true
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ModelInfo = Struct.new(
|
|
27
|
+
:name, :table_name, :columns, :associations, :validations, :scopes, :primary_key,
|
|
28
|
+
keyword_init: true
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def parse
|
|
32
|
+
return {} unless defined?(Rails) && defined?(ActiveRecord::Base)
|
|
33
|
+
|
|
34
|
+
# Eager load all models - force it even in development
|
|
35
|
+
begin
|
|
36
|
+
Rails.application.eager_load!
|
|
37
|
+
rescue => e
|
|
38
|
+
Rails.logger.warn "Could not eager load: #{e.message}" if defined?(Rails.logger)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Also try to load app/models directory explicitly
|
|
42
|
+
if defined?(Rails.root)
|
|
43
|
+
Dir[Rails.root.join('app/models/**/*.rb')].each do |file|
|
|
44
|
+
begin
|
|
45
|
+
require_dependency file
|
|
46
|
+
rescue => e
|
|
47
|
+
# Ignore errors, file might already be loaded
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
models = ActiveRecord::Base.descendants.reject do |model|
|
|
53
|
+
model.abstract_class? ||
|
|
54
|
+
model.name.nil? ||
|
|
55
|
+
model.name.start_with?("HABTM_") ||
|
|
56
|
+
excluded_model?(model)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
models.each_with_object({}) do |model, hash|
|
|
60
|
+
hash[model.name] = parse_model(model)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
# Skip models that can't be introspected
|
|
63
|
+
Rails.logger.warn("Could not parse model #{model.name}: #{e.message}") if defined?(Rails.logger)
|
|
64
|
+
end.sort.to_h
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def parse_model(model)
|
|
70
|
+
ModelInfo.new(
|
|
71
|
+
name: model.name,
|
|
72
|
+
table_name: safe_table_name(model),
|
|
73
|
+
columns: parse_columns(model),
|
|
74
|
+
associations: parse_associations(model),
|
|
75
|
+
validations: parse_validations(model),
|
|
76
|
+
scopes: parse_scopes(model),
|
|
77
|
+
primary_key: safe_primary_key(model)
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def safe_table_name(model)
|
|
82
|
+
model.table_name
|
|
83
|
+
rescue StandardError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def safe_primary_key(model)
|
|
88
|
+
model.primary_key
|
|
89
|
+
rescue StandardError
|
|
90
|
+
"id"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_columns(model)
|
|
94
|
+
return [] unless model.table_exists?
|
|
95
|
+
|
|
96
|
+
model.columns.map do |column|
|
|
97
|
+
ColumnInfo.new(
|
|
98
|
+
name: column.name,
|
|
99
|
+
type: column.type.to_s,
|
|
100
|
+
default: column.default,
|
|
101
|
+
null: column.null,
|
|
102
|
+
limit: column.limit,
|
|
103
|
+
precision: column.precision,
|
|
104
|
+
scale: column.scale
|
|
105
|
+
)
|
|
106
|
+
end.sort_by(&:name)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_associations(model)
|
|
112
|
+
model.reflect_on_all_associations.map do |assoc|
|
|
113
|
+
AssociationInfo.new(
|
|
114
|
+
name: assoc.name.to_s,
|
|
115
|
+
type: assoc.macro.to_s,
|
|
116
|
+
class_name: association_class_name(assoc),
|
|
117
|
+
foreign_key: association_foreign_key(assoc),
|
|
118
|
+
options: extract_association_options(assoc)
|
|
119
|
+
)
|
|
120
|
+
end.sort_by(&:name)
|
|
121
|
+
rescue StandardError
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def association_class_name(assoc)
|
|
126
|
+
assoc.class_name
|
|
127
|
+
rescue StandardError
|
|
128
|
+
assoc.name.to_s.classify
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def association_foreign_key(assoc)
|
|
132
|
+
assoc.foreign_key.to_s
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def extract_association_options(assoc)
|
|
138
|
+
options = {}
|
|
139
|
+
options[:dependent] = assoc.options[:dependent] if assoc.options[:dependent]
|
|
140
|
+
options[:through] = assoc.options[:through].to_s if assoc.options[:through]
|
|
141
|
+
options[:polymorphic] = true if assoc.options[:polymorphic]
|
|
142
|
+
options[:as] = assoc.options[:as].to_s if assoc.options[:as]
|
|
143
|
+
options
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_validations(model)
|
|
147
|
+
return [] unless RailsMap.configuration.include_validations
|
|
148
|
+
|
|
149
|
+
model.validators.flat_map do |validator|
|
|
150
|
+
validator.attributes.map do |attr|
|
|
151
|
+
ValidationInfo.new(
|
|
152
|
+
attribute: attr.to_s,
|
|
153
|
+
kind: validator.kind.to_s,
|
|
154
|
+
options: extract_validation_options(validator)
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
end.sort_by { |v| [v.attribute, v.kind] }
|
|
158
|
+
rescue StandardError
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def extract_validation_options(validator)
|
|
163
|
+
validator.options.reject { |k, _| k == :if || k == :unless }
|
|
164
|
+
.transform_values(&:to_s)
|
|
165
|
+
rescue StandardError
|
|
166
|
+
{}
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_scopes(model)
|
|
170
|
+
return [] unless RailsMap.configuration.include_scopes
|
|
171
|
+
|
|
172
|
+
scope_names = model.methods.grep(/^_scope_/).map { |m| m.to_s.sub("_scope_", "") }
|
|
173
|
+
|
|
174
|
+
# Alternative: check defined scopes
|
|
175
|
+
if model.respond_to?(:defined_scopes)
|
|
176
|
+
scope_names = model.defined_scopes.keys.map(&:to_s)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Fallback: look at singleton methods that return ActiveRecord::Relation
|
|
180
|
+
if scope_names.empty?
|
|
181
|
+
scope_names = extract_scope_names_from_singleton_methods(model)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
scope_names.uniq.sort.map do |name|
|
|
185
|
+
ScopeInfo.new(
|
|
186
|
+
name: name,
|
|
187
|
+
arity: safe_scope_arity(model, name)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
rescue StandardError
|
|
191
|
+
[]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def extract_scope_names_from_singleton_methods(model)
|
|
195
|
+
# This is a heuristic approach
|
|
196
|
+
model.singleton_methods(false).select do |method|
|
|
197
|
+
# Skip common non-scope methods
|
|
198
|
+
!%w[table_name primary_key inheritance_column].include?(method.to_s)
|
|
199
|
+
end.map(&:to_s)
|
|
200
|
+
rescue StandardError
|
|
201
|
+
[]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def safe_scope_arity(model, name)
|
|
205
|
+
model.method(name).arity
|
|
206
|
+
rescue StandardError
|
|
207
|
+
0
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def excluded_model?(model)
|
|
211
|
+
return true if model.name.nil?
|
|
212
|
+
|
|
213
|
+
# Exclude models from gems/engines by namespace
|
|
214
|
+
excluded_namespaces = %w[
|
|
215
|
+
RailsMap
|
|
216
|
+
ActionMailbox
|
|
217
|
+
ActionText
|
|
218
|
+
ActiveStorage
|
|
219
|
+
Turbo
|
|
220
|
+
Devise
|
|
221
|
+
Sidekiq
|
|
222
|
+
SolidQueue
|
|
223
|
+
SolidCache
|
|
224
|
+
GoodJob
|
|
225
|
+
Que
|
|
226
|
+
Delayed
|
|
227
|
+
Flipper
|
|
228
|
+
PgHero
|
|
229
|
+
Blazer
|
|
230
|
+
Avo
|
|
231
|
+
MissionControl
|
|
232
|
+
Administrate
|
|
233
|
+
RailsAdmin
|
|
234
|
+
Ahoy
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
# Check namespace exclusions first (fast check)
|
|
238
|
+
model_name = model.name.to_s
|
|
239
|
+
first_namespace = model_name.split('::').first
|
|
240
|
+
return true if excluded_namespaces.include?(first_namespace)
|
|
241
|
+
return true if excluded_namespaces.any? { |ns| model_name.start_with?("#{ns}::") }
|
|
242
|
+
|
|
243
|
+
# Check if model file exists in app/models of host app
|
|
244
|
+
if defined?(Rails.root)
|
|
245
|
+
# Convert model name to file path (e.g., User -> user.rb, Admin::User -> admin/user.rb)
|
|
246
|
+
model_path = model_name.underscore + '.rb'
|
|
247
|
+
model_file = Rails.root.join('app', 'models', model_path)
|
|
248
|
+
|
|
249
|
+
# If the file doesn't exist in app/models, it's likely from a gem
|
|
250
|
+
return true unless File.exist?(model_file)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
false
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|