docit 0.3.1 → 0.5.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/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
- data/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
- data/CHANGELOG.md +33 -2
- data/CONTRIBUTING.md +1 -1
- data/README.md +41 -14
- data/app/controllers/docit/ui_controller.rb +57 -3
- data/config/routes.rb +3 -0
- data/lib/docit/ai/autodoc_runner.rb +25 -24
- data/lib/docit/ai/system_insight_generator.rb +127 -0
- data/lib/docit/ai/tag_injector.rb +1 -1
- data/lib/docit/ai.rb +1 -0
- data/lib/docit/configuration.rb +37 -1
- data/lib/docit/doc_file.rb +1 -1
- data/lib/docit/dsl.rb +6 -3
- data/lib/docit/operation.rb +7 -2
- data/lib/docit/route_inspector.rb +1 -1
- data/lib/docit/schema_generator.rb +24 -5
- data/lib/docit/system_graph/edge.rb +29 -0
- data/lib/docit/system_graph/generator.rb +15 -0
- data/lib/docit/system_graph/graph.rb +55 -0
- data/lib/docit/system_graph/node.rb +32 -0
- data/lib/docit/system_graph/rails_analyzer.rb +251 -0
- data/lib/docit/system_graph/source_scanner.rb +77 -0
- data/lib/docit/system_graph.rb +8 -0
- data/lib/docit/ui/base_renderer.rb +54 -21
- data/lib/docit/ui/system_renderer.rb +113 -0
- data/lib/docit/ui/system_script.rb +1495 -0
- data/lib/docit/ui/system_styles.rb +582 -0
- data/lib/docit/version.rb +1 -1
- data/lib/docit.rb +4 -0
- data/lib/generators/docit/install/install_generator.rb +3 -3
- data/lib/generators/docit/install/templates/initializer.rb +4 -0
- data/lib/tasks/docit.rake +1 -1
- metadata +15 -6
- data/docs/images/scalar_image.png +0 -0
- data/docs/images/swagger_image.png +0 -0
data/lib/docit/dsl.rb
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Docit
|
|
4
4
|
# Included in all Rails controllers via the Engine.
|
|
5
|
-
# Provides +
|
|
5
|
+
# Provides +doc_for+ and +use_docs+ class methods.
|
|
6
6
|
module DSL
|
|
7
7
|
def self.included(base)
|
|
8
8
|
base.extend(ClassMethods)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
module ClassMethods
|
|
12
|
-
def
|
|
12
|
+
def doc_for(action, &block)
|
|
13
13
|
operation = Operation.new(
|
|
14
14
|
controller: name,
|
|
15
15
|
action: action
|
|
@@ -18,9 +18,12 @@ module Docit
|
|
|
18
18
|
Registry.register(operation)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Backward-compatible alias
|
|
22
|
+
alias swagger_doc doc_for
|
|
23
|
+
|
|
21
24
|
def use_docs(doc_module)
|
|
22
25
|
doc_module.actions.each do |action|
|
|
23
|
-
|
|
26
|
+
doc_for(action, &doc_module[action])
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
end
|
data/lib/docit/operation.rb
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Docit
|
|
4
4
|
# Represents the documentation for a single controller action.
|
|
5
|
-
# Created by +
|
|
5
|
+
# Created by +doc_for+ and stored in the {Registry}.
|
|
6
6
|
class Operation
|
|
7
7
|
attr_reader :controller, :action, :_summary, :_description,
|
|
8
8
|
:_tags, :_responses, :_request_body, :_parameters,
|
|
9
|
-
:_security, :_deprecated
|
|
9
|
+
:_security, :_deprecated, :_operation_id
|
|
10
10
|
|
|
11
11
|
def initialize(controller:, action:)
|
|
12
12
|
@controller = controller
|
|
@@ -17,6 +17,7 @@ module Docit
|
|
|
17
17
|
@_request_body = nil
|
|
18
18
|
@_security = []
|
|
19
19
|
@_deprecated = false
|
|
20
|
+
@_operation_id = nil
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def summary(text)
|
|
@@ -35,6 +36,10 @@ module Docit
|
|
|
35
36
|
@_deprecated = value
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
def operation_id(text)
|
|
40
|
+
@_operation_id = text
|
|
41
|
+
end
|
|
42
|
+
|
|
38
43
|
def security(scheme)
|
|
39
44
|
@_security << scheme
|
|
40
45
|
end
|
|
@@ -5,7 +5,7 @@ module Docit
|
|
|
5
5
|
class RouteInspector
|
|
6
6
|
VALID_METHODS = %w[get post put patch delete].freeze
|
|
7
7
|
|
|
8
|
-
# Eagerly loads controller classes so
|
|
8
|
+
# Eagerly loads controller classes so doc_for/use_docs macros run before spec generation.
|
|
9
9
|
def self.eager_load_controllers!
|
|
10
10
|
return if defined?(Rails).nil? || Rails.application.routes.nil?
|
|
11
11
|
|
|
@@ -12,11 +12,7 @@ module Docit
|
|
|
12
12
|
|
|
13
13
|
spec = {
|
|
14
14
|
openapi: "3.0.3",
|
|
15
|
-
info:
|
|
16
|
-
title: config.title,
|
|
17
|
-
version: config.version,
|
|
18
|
-
description: config.description
|
|
19
|
-
},
|
|
15
|
+
info: build_info(config),
|
|
20
16
|
paths: build_paths,
|
|
21
17
|
components: {
|
|
22
18
|
securitySchemes: config.security_schemes
|
|
@@ -37,6 +33,18 @@ module Docit
|
|
|
37
33
|
|
|
38
34
|
private
|
|
39
35
|
|
|
36
|
+
def build_info(config)
|
|
37
|
+
info = {
|
|
38
|
+
title: config.title,
|
|
39
|
+
version: config.version,
|
|
40
|
+
description: config.description
|
|
41
|
+
}
|
|
42
|
+
info[:license] = config.license_info if config.license_info
|
|
43
|
+
info[:contact] = config.contact_info if config.contact_info
|
|
44
|
+
info[:termsOfService] = config.terms_of_service_url if config.terms_of_service_url
|
|
45
|
+
info
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
def build_paths
|
|
41
49
|
paths = {}
|
|
42
50
|
|
|
@@ -58,6 +66,7 @@ module Docit
|
|
|
58
66
|
|
|
59
67
|
def build_operation(operation)
|
|
60
68
|
result = {
|
|
69
|
+
operationId: operation._operation_id || generate_operation_id(operation),
|
|
61
70
|
summary: operation._summary || operation.action.humanize,
|
|
62
71
|
description: operation._description || "",
|
|
63
72
|
tags: operation._tags,
|
|
@@ -183,5 +192,15 @@ module Docit
|
|
|
183
192
|
hash[ex[:name].to_s] = entry
|
|
184
193
|
end
|
|
185
194
|
end
|
|
195
|
+
|
|
196
|
+
def generate_operation_id(operation)
|
|
197
|
+
# "Api::V1::UsersController" → "users" ; "index" → "listUsers"
|
|
198
|
+
resource = operation.controller
|
|
199
|
+
.gsub(/.*::/, "") # strip namespace
|
|
200
|
+
.gsub(/Controller$/, "") # strip suffix
|
|
201
|
+
action = operation.action
|
|
202
|
+
|
|
203
|
+
"#{action}_#{resource}".gsub("::", "_").downcase
|
|
204
|
+
end
|
|
186
205
|
end
|
|
187
206
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module SystemGraph
|
|
5
|
+
class Edge
|
|
6
|
+
attr_reader :id, :source, :target, :type, :confidence, :evidence
|
|
7
|
+
|
|
8
|
+
def initialize(id:, source:, target:, type:, confidence:, evidence:)
|
|
9
|
+
@id = id.to_s
|
|
10
|
+
@source = source.to_s
|
|
11
|
+
@target = target.to_s
|
|
12
|
+
@type = type.to_s
|
|
13
|
+
@confidence = confidence.to_s
|
|
14
|
+
@evidence = evidence.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
id: id,
|
|
20
|
+
source: source,
|
|
21
|
+
target: target,
|
|
22
|
+
type: type,
|
|
23
|
+
confidence: confidence,
|
|
24
|
+
evidence: evidence
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Docit
|
|
6
|
+
module SystemGraph
|
|
7
|
+
class Graph
|
|
8
|
+
VERSION = "1.0"
|
|
9
|
+
|
|
10
|
+
attr_reader :nodes, :edges, :framework
|
|
11
|
+
|
|
12
|
+
def initialize(framework: "rails")
|
|
13
|
+
@framework = framework
|
|
14
|
+
@nodes = {}
|
|
15
|
+
@edges = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_node(node)
|
|
19
|
+
nodes[node.id] ||= node
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_edge(edge)
|
|
23
|
+
return if edge.source == edge.target
|
|
24
|
+
return unless nodes.key?(edge.source) && nodes.key?(edge.target)
|
|
25
|
+
|
|
26
|
+
edges[edge.id] ||= edge
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
node_values = nodes.values
|
|
31
|
+
edge_values = edges.values
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
version: VERSION,
|
|
35
|
+
generated_at: Time.now.utc.iso8601,
|
|
36
|
+
framework: framework,
|
|
37
|
+
nodes: node_values.map(&:to_h),
|
|
38
|
+
edges: edge_values.map(&:to_h),
|
|
39
|
+
stats: stats(node_values, edge_values)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def stats(node_values, edge_values)
|
|
46
|
+
{
|
|
47
|
+
nodes: node_values.length,
|
|
48
|
+
edges: edge_values.length,
|
|
49
|
+
node_types: node_values.group_by(&:type).transform_values(&:length),
|
|
50
|
+
edge_types: edge_values.group_by(&:type).transform_values(&:length)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module SystemGraph
|
|
5
|
+
class Node
|
|
6
|
+
attr_reader :id, :type, :label, :metadata, :file, :line, :status
|
|
7
|
+
|
|
8
|
+
def initialize(id:, type:, label:, metadata: {}, file: nil, line: nil, status: nil)
|
|
9
|
+
@id = id.to_s
|
|
10
|
+
@type = type.to_s
|
|
11
|
+
@label = label.to_s
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
@file = file
|
|
14
|
+
@line = line
|
|
15
|
+
@status = status
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
id: id,
|
|
21
|
+
type: type,
|
|
22
|
+
label: label,
|
|
23
|
+
metadata: metadata
|
|
24
|
+
}.tap do |hash|
|
|
25
|
+
hash[:file] = file if file
|
|
26
|
+
hash[:line] = line if line
|
|
27
|
+
hash[:status] = status if status
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module SystemGraph
|
|
5
|
+
class RailsAnalyzer
|
|
6
|
+
VALID_METHODS = %w[get post put patch delete].freeze
|
|
7
|
+
SKIP_PREFIXES = %w[docit/ rails/ active_storage/ action_mailbox/].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(graph: Graph.new, scanner: SourceScanner.new(root: Rails.root))
|
|
10
|
+
@graph = graph
|
|
11
|
+
@scanner = scanner
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze
|
|
15
|
+
add_routes_and_actions
|
|
16
|
+
add_schemas
|
|
17
|
+
add_models
|
|
18
|
+
add_source_nodes
|
|
19
|
+
graph
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :graph, :scanner
|
|
25
|
+
|
|
26
|
+
def add_routes_and_actions
|
|
27
|
+
route_infos.each do |info|
|
|
28
|
+
controller_id = node_id("controller", info[:controller])
|
|
29
|
+
action_id = "#{controller_id}##{info[:action]}"
|
|
30
|
+
route_id = "route:#{info[:method]}:#{info[:path]}"
|
|
31
|
+
operation = Registry.find(controller: info[:controller], action: info[:action])
|
|
32
|
+
|
|
33
|
+
graph.add_node(Node.new(
|
|
34
|
+
id: controller_id,
|
|
35
|
+
type: "controller",
|
|
36
|
+
label: info[:controller],
|
|
37
|
+
file: controller_file(info[:controller]),
|
|
38
|
+
metadata: { controller_path: info[:controller_path] }
|
|
39
|
+
))
|
|
40
|
+
graph.add_node(Node.new(
|
|
41
|
+
id: action_id,
|
|
42
|
+
type: "action",
|
|
43
|
+
label: "#{info[:controller]}##{info[:action]}",
|
|
44
|
+
file: controller_file(info[:controller]),
|
|
45
|
+
status: operation ? "documented" : "undocumented",
|
|
46
|
+
metadata: { action: info[:action], http_method: info[:method], path: info[:path] }
|
|
47
|
+
))
|
|
48
|
+
graph.add_node(Node.new(
|
|
49
|
+
id: route_id,
|
|
50
|
+
type: "route",
|
|
51
|
+
label: "#{info[:method].upcase} #{info[:path]}",
|
|
52
|
+
metadata: { method: info[:method], path: info[:path] },
|
|
53
|
+
status: operation ? "documented" : "undocumented"
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
graph.add_edge(edge(route_id, action_id, "routes_to", "high", "Rails route table"))
|
|
57
|
+
graph.add_edge(edge(controller_id, action_id, "contains", "high", "Rails controller action"))
|
|
58
|
+
add_doc_node(info, operation, action_id) if operation
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_doc_node(info, operation, action_id)
|
|
63
|
+
doc_id = "doc:#{info[:controller].underscore}:#{info[:action]}"
|
|
64
|
+
|
|
65
|
+
request_body_info = nil
|
|
66
|
+
if operation._request_body
|
|
67
|
+
request_body_info = {
|
|
68
|
+
required: operation._request_body.required,
|
|
69
|
+
content_type: operation._request_body.content_type,
|
|
70
|
+
schema_ref: operation._request_body.schema_ref,
|
|
71
|
+
properties: operation._request_body.properties
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
responses_info = operation._responses.map do |res|
|
|
76
|
+
{
|
|
77
|
+
status: res.status,
|
|
78
|
+
description: res.description,
|
|
79
|
+
schema_ref: res.schema_ref,
|
|
80
|
+
properties: res.properties,
|
|
81
|
+
examples: res.examples
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
parameters_info = operation._parameters.params.map do |param|
|
|
86
|
+
{
|
|
87
|
+
name: param[:name],
|
|
88
|
+
location: param[:in],
|
|
89
|
+
type: param[:schema][:type],
|
|
90
|
+
required: param[:required],
|
|
91
|
+
description: param[:description]
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
graph.add_node(Node.new(
|
|
96
|
+
id: doc_id,
|
|
97
|
+
type: "doc",
|
|
98
|
+
label: operation._summary || "#{info[:controller]}##{info[:action]}",
|
|
99
|
+
metadata: {
|
|
100
|
+
controller: info[:controller],
|
|
101
|
+
action: info[:action],
|
|
102
|
+
description: operation._description,
|
|
103
|
+
tags: operation._tags,
|
|
104
|
+
request_body: request_body_info,
|
|
105
|
+
responses: responses_info,
|
|
106
|
+
parameters: parameters_info
|
|
107
|
+
},
|
|
108
|
+
status: "documented"
|
|
109
|
+
))
|
|
110
|
+
graph.add_edge(edge(doc_id, action_id, "documents", "high", "Docit registry"))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def add_schemas
|
|
114
|
+
Docit.schemas.each_key do |name|
|
|
115
|
+
graph.add_node(Node.new(
|
|
116
|
+
id: node_id("schema", name),
|
|
117
|
+
type: "schema",
|
|
118
|
+
label: name.to_s,
|
|
119
|
+
metadata: { properties: Docit.schemas[name].properties.map { |prop| prop[:name].to_s } }
|
|
120
|
+
))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def add_models
|
|
125
|
+
return unless defined?(ActiveRecord::Base)
|
|
126
|
+
|
|
127
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
128
|
+
next if model.name.nil?
|
|
129
|
+
|
|
130
|
+
model_id = node_id("model", model.name)
|
|
131
|
+
graph.add_node(Node.new(
|
|
132
|
+
id: model_id,
|
|
133
|
+
type: "model",
|
|
134
|
+
label: model.name,
|
|
135
|
+
file: model_file(model.name),
|
|
136
|
+
metadata: { table_name: table_name(model) }
|
|
137
|
+
))
|
|
138
|
+
add_model_associations(model, model_id)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def add_model_associations(model, model_id)
|
|
143
|
+
return unless model.respond_to?(:reflect_on_all_associations)
|
|
144
|
+
|
|
145
|
+
model.reflect_on_all_associations.each do |association|
|
|
146
|
+
target = association.class_name
|
|
147
|
+
target_id = node_id("model", target)
|
|
148
|
+
next unless graph.nodes.key?(target_id)
|
|
149
|
+
|
|
150
|
+
graph.add_edge(edge(model_id, target_id, "association", "high", "ActiveRecord reflection: #{association.name}"))
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def add_source_nodes
|
|
155
|
+
source_nodes = scanner.source_nodes
|
|
156
|
+
source_nodes.each { |node| graph.add_node(node) }
|
|
157
|
+
|
|
158
|
+
labels = graph.nodes.values.select { |node| %w[model service job mailer].include?(node.type) }.map(&:label)
|
|
159
|
+
graph.nodes.values.select { |node| %w[controller action service job mailer].include?(node.type) }.each do |source|
|
|
160
|
+
scanner.references_for(source.file, labels).each do |label|
|
|
161
|
+
target = graph.nodes.values.find { |node| node.label == label }
|
|
162
|
+
next unless target
|
|
163
|
+
|
|
164
|
+
graph.add_edge(edge(source.id, target.id, edge_type_for(target), "medium", "Constant reference in #{source.file}"))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def route_infos
|
|
170
|
+
return [] if defined?(Rails).nil? || Rails.application.routes.nil?
|
|
171
|
+
|
|
172
|
+
Rails.application.routes.routes.filter_map do |route|
|
|
173
|
+
controller_path = route.defaults[:controller]
|
|
174
|
+
action = route.defaults[:action]
|
|
175
|
+
next if controller_path.nil? || action.nil?
|
|
176
|
+
next if skip_route?(controller_path)
|
|
177
|
+
|
|
178
|
+
method = extract_verb(route)
|
|
179
|
+
next if VALID_METHODS.exclude?(method)
|
|
180
|
+
|
|
181
|
+
controller = "#{controller_path}_controller".camelize
|
|
182
|
+
next if excluded_path?(controller_file(controller))
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
controller: controller,
|
|
186
|
+
controller_path: controller_path,
|
|
187
|
+
action: action.to_s,
|
|
188
|
+
method: method,
|
|
189
|
+
path: normalize_path(route.path.spec.to_s)
|
|
190
|
+
}
|
|
191
|
+
end.uniq
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def edge(source, target, type, confidence, evidence)
|
|
195
|
+
Edge.new(
|
|
196
|
+
id: "#{type}:#{source}->#{target}",
|
|
197
|
+
source: source,
|
|
198
|
+
target: target,
|
|
199
|
+
type: type,
|
|
200
|
+
confidence: confidence,
|
|
201
|
+
evidence: evidence
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def node_id(type, label)
|
|
206
|
+
"#{type}:#{label.to_s.underscore.tr('/', ':')}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def controller_file(controller)
|
|
210
|
+
"app/controllers/#{controller.underscore}.rb"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def model_file(model)
|
|
214
|
+
"app/models/#{model.underscore}.rb"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def edge_type_for(target)
|
|
218
|
+
target.type == "model" ? "uses_model" : "calls"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def table_name(model)
|
|
222
|
+
model.table_name if model.respond_to?(:table_name)
|
|
223
|
+
rescue ActiveRecord::StatementInvalid
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def skip_route?(controller_path)
|
|
228
|
+
SKIP_PREFIXES.any? { |prefix| controller_path.start_with?(prefix) }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def excluded_path?(path)
|
|
232
|
+
Docit.configuration.system_graph_excluded_paths.any? do |excluded|
|
|
233
|
+
path.start_with?(excluded.to_s)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def extract_verb(route)
|
|
238
|
+
verb = route.verb
|
|
239
|
+
verb = verb.source if verb.is_a?(Regexp)
|
|
240
|
+
verb.to_s.downcase.gsub(/[^a-z]/, "")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def normalize_path(path)
|
|
244
|
+
path
|
|
245
|
+
.gsub("(.:format)", "")
|
|
246
|
+
.gsub(/\(\.?:(\w+)\)/, '{\1}')
|
|
247
|
+
.gsub(/:(\w+)/, '{\1}')
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module SystemGraph
|
|
5
|
+
class SourceScanner
|
|
6
|
+
SOURCE_TYPES = {
|
|
7
|
+
"app/services" => "service",
|
|
8
|
+
"app/jobs" => "job",
|
|
9
|
+
"app/mailers" => "mailer"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def initialize(root:, excluded_paths: Docit.configuration.system_graph_excluded_paths)
|
|
13
|
+
@root = root
|
|
14
|
+
@excluded_paths = excluded_paths.map(&:to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def source_nodes
|
|
18
|
+
SOURCE_TYPES.each_with_object([]) do |(dir, type), nodes|
|
|
19
|
+
scan_dir(dir).each do |path|
|
|
20
|
+
relative = relative_path(path)
|
|
21
|
+
label = constant_name(relative, dir)
|
|
22
|
+
nodes << Node.new(
|
|
23
|
+
id: node_id(type, label),
|
|
24
|
+
type: type,
|
|
25
|
+
label: label,
|
|
26
|
+
file: relative,
|
|
27
|
+
metadata: { path: relative }
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def references_for(path, candidates)
|
|
34
|
+
return [] unless path && File.exist?(full_path(path))
|
|
35
|
+
|
|
36
|
+
content = File.read(full_path(path))
|
|
37
|
+
candidates.select do |candidate|
|
|
38
|
+
content.match?(/\b#{Regexp.escape(candidate)}\b/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :root
|
|
45
|
+
attr_reader :excluded_paths
|
|
46
|
+
|
|
47
|
+
def scan_dir(dir)
|
|
48
|
+
full_dir = root.join(dir)
|
|
49
|
+
return [] unless Dir.exist?(full_dir)
|
|
50
|
+
|
|
51
|
+
Dir.glob(full_dir.join("**", "*.rb")).sort.reject do |path|
|
|
52
|
+
excluded?(relative_path(path))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def relative_path(path)
|
|
57
|
+
path.to_s.sub("#{root}/", "")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def constant_name(relative, dir)
|
|
61
|
+
relative.delete_prefix("#{dir}/").delete_suffix(".rb").camelize
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def node_id(type, label)
|
|
65
|
+
"#{type}:#{label.underscore.tr('/', ':')}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def full_path(path)
|
|
69
|
+
root.join(path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def excluded?(path)
|
|
73
|
+
excluded_paths.any? { |excluded| path.start_with?(excluded) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "system_graph/node"
|
|
4
|
+
require_relative "system_graph/edge"
|
|
5
|
+
require_relative "system_graph/graph"
|
|
6
|
+
require_relative "system_graph/source_scanner"
|
|
7
|
+
require_relative "system_graph/rails_analyzer"
|
|
8
|
+
require_relative "system_graph/generator"
|