woods 1.0.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/CHANGELOG.md +89 -0
- data/CODE_OF_CONDUCT.md +83 -0
- data/CONTRIBUTING.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +406 -0
- data/exe/woods-console +59 -0
- data/exe/woods-console-mcp +22 -0
- data/exe/woods-mcp +34 -0
- data/exe/woods-mcp-http +37 -0
- data/exe/woods-mcp-start +58 -0
- data/lib/generators/woods/install_generator.rb +32 -0
- data/lib/generators/woods/pgvector_generator.rb +37 -0
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +15 -0
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +43 -0
- data/lib/tasks/woods.rake +621 -0
- data/lib/tasks/woods_evaluation.rake +115 -0
- data/lib/woods/ast/call_site_extractor.rb +106 -0
- data/lib/woods/ast/method_extractor.rb +71 -0
- data/lib/woods/ast/node.rb +116 -0
- data/lib/woods/ast/parser.rb +614 -0
- data/lib/woods/ast.rb +6 -0
- data/lib/woods/builder.rb +200 -0
- data/lib/woods/cache/cache_middleware.rb +199 -0
- data/lib/woods/cache/cache_store.rb +264 -0
- data/lib/woods/cache/redis_cache_store.rb +116 -0
- data/lib/woods/cache/solid_cache_store.rb +111 -0
- data/lib/woods/chunking/chunk.rb +84 -0
- data/lib/woods/chunking/semantic_chunker.rb +295 -0
- data/lib/woods/console/adapters/cache_adapter.rb +58 -0
- data/lib/woods/console/adapters/good_job_adapter.rb +33 -0
- data/lib/woods/console/adapters/job_adapter.rb +68 -0
- data/lib/woods/console/adapters/sidekiq_adapter.rb +33 -0
- data/lib/woods/console/adapters/solid_queue_adapter.rb +33 -0
- data/lib/woods/console/audit_logger.rb +75 -0
- data/lib/woods/console/bridge.rb +177 -0
- data/lib/woods/console/confirmation.rb +90 -0
- data/lib/woods/console/connection_manager.rb +173 -0
- data/lib/woods/console/console_response_renderer.rb +74 -0
- data/lib/woods/console/embedded_executor.rb +373 -0
- data/lib/woods/console/model_validator.rb +81 -0
- data/lib/woods/console/rack_middleware.rb +87 -0
- data/lib/woods/console/safe_context.rb +82 -0
- data/lib/woods/console/server.rb +612 -0
- data/lib/woods/console/sql_validator.rb +172 -0
- data/lib/woods/console/tools/tier1.rb +118 -0
- data/lib/woods/console/tools/tier2.rb +117 -0
- data/lib/woods/console/tools/tier3.rb +110 -0
- data/lib/woods/console/tools/tier4.rb +79 -0
- data/lib/woods/coordination/pipeline_lock.rb +109 -0
- data/lib/woods/cost_model/embedding_cost.rb +88 -0
- data/lib/woods/cost_model/estimator.rb +128 -0
- data/lib/woods/cost_model/provider_pricing.rb +67 -0
- data/lib/woods/cost_model/storage_cost.rb +52 -0
- data/lib/woods/cost_model.rb +22 -0
- data/lib/woods/db/migrations/001_create_units.rb +38 -0
- data/lib/woods/db/migrations/002_create_edges.rb +35 -0
- data/lib/woods/db/migrations/003_create_embeddings.rb +37 -0
- data/lib/woods/db/migrations/004_create_snapshots.rb +45 -0
- data/lib/woods/db/migrations/005_create_snapshot_units.rb +40 -0
- data/lib/woods/db/migrations/006_rename_tables.rb +34 -0
- data/lib/woods/db/migrator.rb +73 -0
- data/lib/woods/db/schema_version.rb +73 -0
- data/lib/woods/dependency_graph.rb +236 -0
- data/lib/woods/embedding/indexer.rb +140 -0
- data/lib/woods/embedding/openai.rb +126 -0
- data/lib/woods/embedding/provider.rb +162 -0
- data/lib/woods/embedding/text_preparer.rb +112 -0
- data/lib/woods/evaluation/baseline_runner.rb +115 -0
- data/lib/woods/evaluation/evaluator.rb +139 -0
- data/lib/woods/evaluation/metrics.rb +79 -0
- data/lib/woods/evaluation/query_set.rb +148 -0
- data/lib/woods/evaluation/report_generator.rb +90 -0
- data/lib/woods/extracted_unit.rb +145 -0
- data/lib/woods/extractor.rb +1028 -0
- data/lib/woods/extractors/action_cable_extractor.rb +201 -0
- data/lib/woods/extractors/ast_source_extraction.rb +46 -0
- data/lib/woods/extractors/behavioral_profile.rb +309 -0
- data/lib/woods/extractors/caching_extractor.rb +261 -0
- data/lib/woods/extractors/callback_analyzer.rb +246 -0
- data/lib/woods/extractors/concern_extractor.rb +292 -0
- data/lib/woods/extractors/configuration_extractor.rb +219 -0
- data/lib/woods/extractors/controller_extractor.rb +404 -0
- data/lib/woods/extractors/database_view_extractor.rb +278 -0
- data/lib/woods/extractors/decorator_extractor.rb +253 -0
- data/lib/woods/extractors/engine_extractor.rb +223 -0
- data/lib/woods/extractors/event_extractor.rb +211 -0
- data/lib/woods/extractors/factory_extractor.rb +289 -0
- data/lib/woods/extractors/graphql_extractor.rb +892 -0
- data/lib/woods/extractors/i18n_extractor.rb +117 -0
- data/lib/woods/extractors/job_extractor.rb +374 -0
- data/lib/woods/extractors/lib_extractor.rb +218 -0
- data/lib/woods/extractors/mailer_extractor.rb +269 -0
- data/lib/woods/extractors/manager_extractor.rb +188 -0
- data/lib/woods/extractors/middleware_extractor.rb +133 -0
- data/lib/woods/extractors/migration_extractor.rb +469 -0
- data/lib/woods/extractors/model_extractor.rb +988 -0
- data/lib/woods/extractors/phlex_extractor.rb +252 -0
- data/lib/woods/extractors/policy_extractor.rb +191 -0
- data/lib/woods/extractors/poro_extractor.rb +229 -0
- data/lib/woods/extractors/pundit_extractor.rb +223 -0
- data/lib/woods/extractors/rails_source_extractor.rb +473 -0
- data/lib/woods/extractors/rake_task_extractor.rb +343 -0
- data/lib/woods/extractors/route_extractor.rb +181 -0
- data/lib/woods/extractors/scheduled_job_extractor.rb +331 -0
- data/lib/woods/extractors/serializer_extractor.rb +339 -0
- data/lib/woods/extractors/service_extractor.rb +217 -0
- data/lib/woods/extractors/shared_dependency_scanner.rb +91 -0
- data/lib/woods/extractors/shared_utility_methods.rb +281 -0
- data/lib/woods/extractors/state_machine_extractor.rb +398 -0
- data/lib/woods/extractors/test_mapping_extractor.rb +225 -0
- data/lib/woods/extractors/validator_extractor.rb +211 -0
- data/lib/woods/extractors/view_component_extractor.rb +311 -0
- data/lib/woods/extractors/view_template_extractor.rb +261 -0
- data/lib/woods/feedback/gap_detector.rb +89 -0
- data/lib/woods/feedback/store.rb +119 -0
- data/lib/woods/filename_utils.rb +32 -0
- data/lib/woods/flow_analysis/operation_extractor.rb +206 -0
- data/lib/woods/flow_analysis/response_code_mapper.rb +154 -0
- data/lib/woods/flow_assembler.rb +290 -0
- data/lib/woods/flow_document.rb +191 -0
- data/lib/woods/flow_precomputer.rb +102 -0
- data/lib/woods/formatting/base.rb +30 -0
- data/lib/woods/formatting/claude_adapter.rb +98 -0
- data/lib/woods/formatting/generic_adapter.rb +56 -0
- data/lib/woods/formatting/gpt_adapter.rb +64 -0
- data/lib/woods/formatting/human_adapter.rb +78 -0
- data/lib/woods/graph_analyzer.rb +374 -0
- data/lib/woods/mcp/bootstrapper.rb +96 -0
- data/lib/woods/mcp/index_reader.rb +394 -0
- data/lib/woods/mcp/renderers/claude_renderer.rb +81 -0
- data/lib/woods/mcp/renderers/json_renderer.rb +17 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +353 -0
- data/lib/woods/mcp/renderers/plain_renderer.rb +240 -0
- data/lib/woods/mcp/server.rb +962 -0
- data/lib/woods/mcp/tool_response_renderer.rb +85 -0
- data/lib/woods/model_name_cache.rb +51 -0
- data/lib/woods/notion/client.rb +217 -0
- data/lib/woods/notion/exporter.rb +219 -0
- data/lib/woods/notion/mapper.rb +40 -0
- data/lib/woods/notion/mappers/column_mapper.rb +57 -0
- data/lib/woods/notion/mappers/migration_mapper.rb +39 -0
- data/lib/woods/notion/mappers/model_mapper.rb +161 -0
- data/lib/woods/notion/mappers/shared.rb +22 -0
- data/lib/woods/notion/rate_limiter.rb +68 -0
- data/lib/woods/observability/health_check.rb +79 -0
- data/lib/woods/observability/instrumentation.rb +34 -0
- data/lib/woods/observability/structured_logger.rb +57 -0
- data/lib/woods/operator/error_escalator.rb +81 -0
- data/lib/woods/operator/pipeline_guard.rb +92 -0
- data/lib/woods/operator/status_reporter.rb +80 -0
- data/lib/woods/railtie.rb +38 -0
- data/lib/woods/resilience/circuit_breaker.rb +99 -0
- data/lib/woods/resilience/index_validator.rb +167 -0
- data/lib/woods/resilience/retryable_provider.rb +108 -0
- data/lib/woods/retrieval/context_assembler.rb +261 -0
- data/lib/woods/retrieval/query_classifier.rb +133 -0
- data/lib/woods/retrieval/ranker.rb +277 -0
- data/lib/woods/retrieval/search_executor.rb +316 -0
- data/lib/woods/retriever.rb +152 -0
- data/lib/woods/ruby_analyzer/class_analyzer.rb +170 -0
- data/lib/woods/ruby_analyzer/dataflow_analyzer.rb +77 -0
- data/lib/woods/ruby_analyzer/fqn_builder.rb +18 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +280 -0
- data/lib/woods/ruby_analyzer/method_analyzer.rb +143 -0
- data/lib/woods/ruby_analyzer/trace_enricher.rb +143 -0
- data/lib/woods/ruby_analyzer.rb +87 -0
- data/lib/woods/session_tracer/file_store.rb +104 -0
- data/lib/woods/session_tracer/middleware.rb +143 -0
- data/lib/woods/session_tracer/redis_store.rb +106 -0
- data/lib/woods/session_tracer/session_flow_assembler.rb +254 -0
- data/lib/woods/session_tracer/session_flow_document.rb +223 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +139 -0
- data/lib/woods/session_tracer/store.rb +81 -0
- data/lib/woods/storage/graph_store.rb +120 -0
- data/lib/woods/storage/metadata_store.rb +196 -0
- data/lib/woods/storage/pgvector.rb +195 -0
- data/lib/woods/storage/qdrant.rb +205 -0
- data/lib/woods/storage/vector_store.rb +167 -0
- data/lib/woods/temporal/json_snapshot_store.rb +245 -0
- data/lib/woods/temporal/snapshot_store.rb +345 -0
- data/lib/woods/token_utils.rb +19 -0
- data/lib/woods/version.rb +5 -0
- data/lib/woods.rb +246 -0
- metadata +270 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'shared_utility_methods'
|
|
4
|
+
require_relative 'shared_dependency_scanner'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module Extractors
|
|
8
|
+
# FactoryExtractor handles extraction of FactoryBot factory definitions.
|
|
9
|
+
#
|
|
10
|
+
# Scans spec/factories/ and test/factories/ for FactoryBot definitions
|
|
11
|
+
# and produces one ExtractedUnit per factory block. Uses a line-by-line
|
|
12
|
+
# state machine parser (never evals factory files).
|
|
13
|
+
#
|
|
14
|
+
# Supports: basic factories, explicit class override, traits, associations,
|
|
15
|
+
# sequences, callbacks, parent inheritance, transient attributes, and
|
|
16
|
+
# nested factory definitions (each becomes its own unit).
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# extractor = FactoryExtractor.new
|
|
20
|
+
# units = extractor.extract_all
|
|
21
|
+
# user = units.find { |u| u.identifier == "user" }
|
|
22
|
+
# user.metadata[:traits] # => ["admin", "with_avatar"]
|
|
23
|
+
#
|
|
24
|
+
class FactoryExtractor
|
|
25
|
+
include SharedUtilityMethods
|
|
26
|
+
include SharedDependencyScanner
|
|
27
|
+
|
|
28
|
+
FACTORY_DIRECTORIES = %w[spec/factories test/factories].freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@directories = FACTORY_DIRECTORIES.map { |d| Rails.root.join(d) }.select(&:directory?)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extract all factory definitions from all discovered directories.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<ExtractedUnit>] List of factory units
|
|
37
|
+
def extract_all
|
|
38
|
+
@directories.flat_map do |dir|
|
|
39
|
+
Dir[dir.join('**/*.rb')].flat_map { |file| extract_factory_file(file) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Extract factory definitions from a single factory file.
|
|
44
|
+
#
|
|
45
|
+
# Returns an Array because each file may contain multiple factory definitions.
|
|
46
|
+
#
|
|
47
|
+
# @param file_path [String] Path to the factory file
|
|
48
|
+
# @return [Array<ExtractedUnit>] List of factory units
|
|
49
|
+
def extract_factory_file(file_path)
|
|
50
|
+
return [] unless file_path.to_s.end_with?('.rb')
|
|
51
|
+
|
|
52
|
+
source = File.read(file_path)
|
|
53
|
+
factories = parse_factories(source)
|
|
54
|
+
|
|
55
|
+
factories.map { |factory_data| build_unit(factory_data, file_path, source) }
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
Rails.logger.error("Failed to extract factories from #{file_path}: #{e.message}")
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Parse factory definitions from source using a line-by-line state machine.
|
|
64
|
+
#
|
|
65
|
+
# Tracks factory nesting, traits, associations, sequences, callbacks, and
|
|
66
|
+
# transient attributes. Each factory block (including nested factories within
|
|
67
|
+
# a parent factory) produces one entry in the returned array.
|
|
68
|
+
#
|
|
69
|
+
# @param source [String] Factory file source code
|
|
70
|
+
# @return [Array<Hash>] Parsed factory data hashes
|
|
71
|
+
def parse_factories(source)
|
|
72
|
+
completed = []
|
|
73
|
+
factory_stack = []
|
|
74
|
+
depth = 0
|
|
75
|
+
in_transient = false
|
|
76
|
+
transient_depth = nil
|
|
77
|
+
|
|
78
|
+
source.lines.each_with_index do |line, index|
|
|
79
|
+
stripped = line.strip
|
|
80
|
+
|
|
81
|
+
# Factory definition — push new factory onto stack
|
|
82
|
+
if (factory_data = match_factory(stripped, depth, index + 1))
|
|
83
|
+
factory_stack.push(factory_data)
|
|
84
|
+
depth += 1
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Trait definition — record trait in current factory, open block
|
|
89
|
+
if (trait_match = stripped.match(/\Atrait\s+:(\w+)\s+do/))
|
|
90
|
+
factory_stack.last[:traits] << trait_match[1] if factory_stack.any?
|
|
91
|
+
depth += 1
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Transient block — start collecting transient attributes
|
|
96
|
+
if stripped.match?(/\Atransient\s+do/)
|
|
97
|
+
in_transient = true
|
|
98
|
+
transient_depth = depth
|
|
99
|
+
depth += 1
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Collect transient attribute names (word { ... } or word do)
|
|
104
|
+
if in_transient && factory_stack.any? && (attr_match = stripped.match(/\A(\w+)\s*(?:\{|do\b)/))
|
|
105
|
+
factory_stack.last[:transient_attributes] << attr_match[1]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Association
|
|
109
|
+
if factory_stack.any? && (assoc_match = stripped.match(/\Aassociation\s+:(\w+)/))
|
|
110
|
+
factory_stack.last[:associations] << assoc_match[1]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Sequence
|
|
114
|
+
if factory_stack.any? && (seq_match = stripped.match(/\Asequence\s*\(:(\w+)\)/))
|
|
115
|
+
factory_stack.last[:sequences] << seq_match[1]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Callbacks: after(:hook), before(:hook), after_stub(:hook)
|
|
119
|
+
if factory_stack.any? && (cb_match = stripped.match(/\A(?:after|before|after_stub)\s*\([:'"](\w+)/))
|
|
120
|
+
factory_stack.last[:callbacks] << cb_match[1]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Generic block openers — factory/trait/transient already handled above with next
|
|
124
|
+
if block_opener?(stripped)
|
|
125
|
+
depth += 1
|
|
126
|
+
next
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
next unless stripped == 'end'
|
|
130
|
+
|
|
131
|
+
depth -= 1
|
|
132
|
+
|
|
133
|
+
# Close transient block if we've returned to the depth where it was opened
|
|
134
|
+
if in_transient && depth == transient_depth
|
|
135
|
+
in_transient = false
|
|
136
|
+
transient_depth = nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Close factory if top factory was opened at this depth
|
|
140
|
+
next unless factory_stack.any? && depth == factory_stack.last[:open_depth]
|
|
141
|
+
|
|
142
|
+
completed << factory_stack.pop
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
completed
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Try to match a factory definition line and return initialized factory data.
|
|
149
|
+
#
|
|
150
|
+
# Handles:
|
|
151
|
+
# factory :name do
|
|
152
|
+
# factory :name, class: ClassName do
|
|
153
|
+
# factory :name, class: 'ClassName' do
|
|
154
|
+
# factory :name, parent: :other do
|
|
155
|
+
#
|
|
156
|
+
# @param line [String] Stripped source line
|
|
157
|
+
# @param depth [Integer] Current block depth when factory would be opened
|
|
158
|
+
# @param line_number [Integer] 1-based line number
|
|
159
|
+
# @return [Hash, nil] Initialized factory data or nil if not a factory line
|
|
160
|
+
def match_factory(line, depth, line_number)
|
|
161
|
+
return nil unless line.match?(/\Afactory\s+:/) && line.match?(/\bdo\b/)
|
|
162
|
+
|
|
163
|
+
name_match = line.match(/\Afactory\s+:(\w+)/)
|
|
164
|
+
return nil unless name_match
|
|
165
|
+
|
|
166
|
+
name = name_match[1]
|
|
167
|
+
options = {}
|
|
168
|
+
|
|
169
|
+
if (class_match = line.match(/\bclass:\s*['"]?([\w:]+)['"]?/))
|
|
170
|
+
options[:class_name] = class_match[1]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if (parent_match = line.match(/\bparent:\s*:(\w+)/))
|
|
174
|
+
options[:parent] = parent_match[1]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
name: name,
|
|
179
|
+
class_name: options[:class_name] || classify(name),
|
|
180
|
+
parent_factory: options[:parent],
|
|
181
|
+
open_depth: depth,
|
|
182
|
+
line_number: line_number,
|
|
183
|
+
traits: [],
|
|
184
|
+
associations: [],
|
|
185
|
+
sequences: [],
|
|
186
|
+
callbacks: [],
|
|
187
|
+
transient_attributes: []
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Convert a snake_case factory name to a CamelCase class name.
|
|
192
|
+
#
|
|
193
|
+
# @param name [String] Snake_case factory name (e.g., "admin_user")
|
|
194
|
+
# @return [String] CamelCase class name (e.g., "AdminUser")
|
|
195
|
+
def classify(name)
|
|
196
|
+
name.split('_').map(&:capitalize).join
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Check if a stripped line opens a new block.
|
|
200
|
+
#
|
|
201
|
+
# Excludes factory, trait, and transient lines — those are handled
|
|
202
|
+
# explicitly in the main parser loop with depth tracking of their own.
|
|
203
|
+
#
|
|
204
|
+
# @param stripped [String] Stripped line content
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
def block_opener?(stripped)
|
|
207
|
+
return false if stripped.match?(/\Afactory\s+:/)
|
|
208
|
+
return false if stripped.match?(/\Atrait\s+:/)
|
|
209
|
+
return false if stripped.match?(/\Atransient\s+do/)
|
|
210
|
+
return true if stripped.match?(/\b(do|def|case|begin|class|module|while|until|for)\b.*(?<!\bend)\s*$/)
|
|
211
|
+
|
|
212
|
+
stripped.match?(/\A(if|unless)\b/)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Build an ExtractedUnit from parsed factory data.
|
|
216
|
+
#
|
|
217
|
+
# @param factory_data [Hash] Parsed factory data
|
|
218
|
+
# @param file_path [String] Path to the factory file
|
|
219
|
+
# @param file_source [String] Full file source
|
|
220
|
+
# @return [ExtractedUnit]
|
|
221
|
+
def build_unit(factory_data, file_path, file_source)
|
|
222
|
+
unit = ExtractedUnit.new(
|
|
223
|
+
type: :factory,
|
|
224
|
+
identifier: factory_data[:name],
|
|
225
|
+
file_path: file_path
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
unit.source_code = build_source_annotation(factory_data, file_source)
|
|
229
|
+
unit.metadata = build_metadata(factory_data)
|
|
230
|
+
unit.dependencies = extract_dependencies(factory_data)
|
|
231
|
+
|
|
232
|
+
unit
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Build annotated source code for the unit.
|
|
236
|
+
#
|
|
237
|
+
# @param factory_data [Hash] Parsed factory data
|
|
238
|
+
# @param file_source [String] Full file source
|
|
239
|
+
# @return [String]
|
|
240
|
+
def build_source_annotation(factory_data, file_source)
|
|
241
|
+
header = "# Factory: #{factory_data[:name]} (model: #{factory_data[:class_name]})"
|
|
242
|
+
header += "\n# Parent: #{factory_data[:parent_factory]}" if factory_data[:parent_factory]
|
|
243
|
+
"#{header}\n#{file_source}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Build metadata hash for the unit.
|
|
247
|
+
#
|
|
248
|
+
# @param factory_data [Hash] Parsed factory data
|
|
249
|
+
# @return [Hash]
|
|
250
|
+
def build_metadata(factory_data)
|
|
251
|
+
{
|
|
252
|
+
factory_name: factory_data[:name],
|
|
253
|
+
model_class: factory_data[:class_name],
|
|
254
|
+
traits: factory_data[:traits],
|
|
255
|
+
associations: factory_data[:associations],
|
|
256
|
+
sequences: factory_data[:sequences],
|
|
257
|
+
parent_factory: factory_data[:parent_factory],
|
|
258
|
+
callbacks: factory_data[:callbacks].uniq,
|
|
259
|
+
transient_attributes: factory_data[:transient_attributes]
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Extract dependencies from factory data.
|
|
264
|
+
#
|
|
265
|
+
# Creates:
|
|
266
|
+
# - :model dependency (via :factory_for) linking to the modeled class
|
|
267
|
+
# - :factory dependency (via :factory_parent) for parent factory inheritance
|
|
268
|
+
# - :factory dependencies (via :factory_association) for each association
|
|
269
|
+
#
|
|
270
|
+
# @param factory_data [Hash] Parsed factory data
|
|
271
|
+
# @return [Array<Hash>]
|
|
272
|
+
def extract_dependencies(factory_data)
|
|
273
|
+
deps = []
|
|
274
|
+
|
|
275
|
+
deps << { type: :model, target: factory_data[:class_name], via: :factory_for }
|
|
276
|
+
|
|
277
|
+
if factory_data[:parent_factory]
|
|
278
|
+
deps << { type: :factory, target: factory_data[:parent_factory], via: :factory_parent }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
factory_data[:associations].each do |assoc|
|
|
282
|
+
deps << { type: :factory, target: assoc, via: :factory_association }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
deps.uniq { |d| [d[:type], d[:target]] }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|