noiseless 0.0.0 → 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.
- checksums.yaml +4 -4
- data/LICENSE.txt +28 -0
- data/README.md +214 -0
- data/lib/application_search.rb +15 -0
- data/lib/noiseless/adapter.rb +313 -0
- data/lib/noiseless/adapters/elasticsearch.rb +70 -0
- data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +188 -0
- data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +377 -0
- data/lib/noiseless/adapters/execution_modules/pgvector_support.rb +219 -0
- data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +461 -0
- data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +472 -0
- data/lib/noiseless/adapters/open_search.rb +208 -0
- data/lib/noiseless/adapters/postgresql.rb +171 -0
- data/lib/noiseless/adapters/typesense.rb +70 -0
- data/lib/noiseless/adapters.rb +14 -0
- data/lib/noiseless/ast/aggregation.rb +56 -0
- data/lib/noiseless/ast/bool.rb +16 -0
- data/lib/noiseless/ast/bulk.rb +18 -0
- data/lib/noiseless/ast/collapse.rb +16 -0
- data/lib/noiseless/ast/combined_fields.rb +33 -0
- data/lib/noiseless/ast/conversation.rb +29 -0
- data/lib/noiseless/ast/filter.rb +15 -0
- data/lib/noiseless/ast/hybrid.rb +35 -0
- data/lib/noiseless/ast/image_query.rb +29 -0
- data/lib/noiseless/ast/join.rb +31 -0
- data/lib/noiseless/ast/match.rb +15 -0
- data/lib/noiseless/ast/multi_match.rb +24 -0
- data/lib/noiseless/ast/paginate.rb +15 -0
- data/lib/noiseless/ast/prefix.rb +15 -0
- data/lib/noiseless/ast/range.rb +18 -0
- data/lib/noiseless/ast/root.rb +69 -0
- data/lib/noiseless/ast/search_after.rb +14 -0
- data/lib/noiseless/ast/sort.rb +15 -0
- data/lib/noiseless/ast/vector.rb +27 -0
- data/lib/noiseless/ast/wildcard.rb +15 -0
- data/lib/noiseless/ast.rb +30 -0
- data/lib/noiseless/bulk_importer.rb +195 -0
- data/lib/noiseless/callbacks.rb +138 -0
- data/lib/noiseless/connection_manager.rb +26 -0
- data/lib/noiseless/document_manager.rb +137 -0
- data/lib/noiseless/dsl.rb +107 -0
- data/lib/noiseless/generators/application_search_generator.rb +24 -0
- data/lib/noiseless/instrumentation.rb +174 -0
- data/lib/noiseless/introspection/console.rb +228 -0
- data/lib/noiseless/introspection/query_visualizer.rb +533 -0
- data/lib/noiseless/introspection.rb +221 -0
- data/lib/noiseless/mapping.rb +253 -0
- data/lib/noiseless/mapping_definition_processor.rb +231 -0
- data/lib/noiseless/model.rb +111 -0
- data/lib/noiseless/model_registry.rb +77 -0
- data/lib/noiseless/multi_search.rb +244 -0
- data/lib/noiseless/pagination.rb +375 -0
- data/lib/noiseless/query_builder.rb +284 -0
- data/lib/noiseless/railtie.rb +35 -0
- data/lib/noiseless/response/aggregations.rb +46 -0
- data/lib/noiseless/response/empty.rb +20 -0
- data/lib/noiseless/response/records.rb +94 -0
- data/lib/noiseless/response/results.rb +110 -0
- data/lib/noiseless/response/suggestions.rb +55 -0
- data/lib/noiseless/response.rb +98 -0
- data/lib/noiseless/response_factory.rb +32 -0
- data/lib/noiseless/runtime_reset_middleware.rb +15 -0
- data/lib/noiseless/search_index_update_job.rb +84 -0
- data/lib/noiseless/test_case.rb +230 -0
- data/lib/noiseless/test_helper.rb +295 -0
- data/lib/noiseless/version.rb +2 -2
- data/lib/noiseless.rb +130 -2
- data/lib/tasks/benchmark.rake +35 -0
- data/lib/tasks/release.rake +22 -0
- data/lib/tasks/test.rake +11 -0
- metadata +260 -14
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mermaid"
|
|
5
|
+
DIAGRAMS_AVAILABLE = true
|
|
6
|
+
rescue LoadError
|
|
7
|
+
DIAGRAMS_AVAILABLE = false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module Noiseless
|
|
11
|
+
module Introspection
|
|
12
|
+
class QueryVisualizer
|
|
13
|
+
def self.compare_across_engines(ast_node, **_opts)
|
|
14
|
+
adapters = [
|
|
15
|
+
{ name: :elasticsearch, class: Noiseless::Adapters::Elasticsearch },
|
|
16
|
+
{ name: :opensearch, class: Noiseless::Adapters::OpenSearch },
|
|
17
|
+
{ name: :typesense, class: Noiseless::Adapters::Typesense }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
comparison = {
|
|
21
|
+
original_ast: ast_node.to_h,
|
|
22
|
+
engine_translations: {},
|
|
23
|
+
compatibility_analysis: {},
|
|
24
|
+
recommendations: []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
adapters.each do |adapter_config|
|
|
28
|
+
adapter = adapter_config[:class].new
|
|
29
|
+
|
|
30
|
+
# Get the translated query
|
|
31
|
+
engine_query = adapter.send(:ast_to_hash, ast_node)
|
|
32
|
+
|
|
33
|
+
# Get adapter info
|
|
34
|
+
adapter_info = adapter.adapter_info
|
|
35
|
+
|
|
36
|
+
comparison[:engine_translations][adapter_config[:name]] = {
|
|
37
|
+
engine_query: engine_query,
|
|
38
|
+
adapter_info: adapter_info,
|
|
39
|
+
query_differences: analyze_query_differences(ast_node.to_h, engine_query),
|
|
40
|
+
estimated_performance: estimate_performance(adapter_info, engine_query)
|
|
41
|
+
}
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
comparison[:engine_translations][adapter_config[:name]] = {
|
|
44
|
+
error: e.message,
|
|
45
|
+
available: false
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Analyze compatibility across engines
|
|
50
|
+
comparison[:compatibility_analysis] = analyze_cross_engine_compatibility(comparison[:engine_translations])
|
|
51
|
+
|
|
52
|
+
# Generate recommendations
|
|
53
|
+
comparison[:recommendations] = generate_recommendations(comparison)
|
|
54
|
+
|
|
55
|
+
comparison
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.visualize_ast(ast_node, format: :tree)
|
|
59
|
+
case format
|
|
60
|
+
when :tree
|
|
61
|
+
visualize_as_tree(ast_node.to_h)
|
|
62
|
+
when :json
|
|
63
|
+
JSON.pretty_generate(ast_node.to_h)
|
|
64
|
+
when :yaml
|
|
65
|
+
YAML.dump(ast_node.to_h)
|
|
66
|
+
when :mermaid
|
|
67
|
+
ast_to_mermaid_flowchart(ast_node)
|
|
68
|
+
when :mermaid_class
|
|
69
|
+
ast_to_mermaid_class_diagram(ast_node)
|
|
70
|
+
else
|
|
71
|
+
raise ArgumentError, "Unsupported format: #{format}. Available: :tree, :json, :yaml, :mermaid, :mermaid_class"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.explain_query_flow(ast_node, adapter)
|
|
76
|
+
explanation = adapter.explain_query(ast_node)
|
|
77
|
+
|
|
78
|
+
flow_diagram = generate_flow_diagram(explanation)
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
explanation: explanation,
|
|
82
|
+
visual_flow: flow_diagram,
|
|
83
|
+
performance_breakdown: format_performance_breakdown(explanation[:performance]),
|
|
84
|
+
optimization_suggestions: suggest_optimizations(explanation)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.analyze_query_differences(original_ast, engine_query)
|
|
89
|
+
differences = []
|
|
90
|
+
|
|
91
|
+
# Check for field mapping differences
|
|
92
|
+
original_fields = extract_fields_from_ast(original_ast)
|
|
93
|
+
engine_fields = extract_fields_from_query(engine_query)
|
|
94
|
+
|
|
95
|
+
if original_fields != engine_fields
|
|
96
|
+
differences << {
|
|
97
|
+
type: :field_mapping,
|
|
98
|
+
original: original_fields,
|
|
99
|
+
engine: engine_fields,
|
|
100
|
+
impact: :medium
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check for query structure differences
|
|
105
|
+
if has_structural_differences?(original_ast, engine_query)
|
|
106
|
+
differences << {
|
|
107
|
+
type: :structural_change,
|
|
108
|
+
description: "Query structure adapted for engine compatibility",
|
|
109
|
+
impact: :low
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
differences
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.estimate_performance(adapter_info, engine_query)
|
|
117
|
+
base_score = 100
|
|
118
|
+
|
|
119
|
+
# Adjust based on query complexity
|
|
120
|
+
complexity_penalty = calculate_query_complexity(engine_query) * 5
|
|
121
|
+
base_score -= complexity_penalty
|
|
122
|
+
|
|
123
|
+
# Adjust based on adapter capabilities
|
|
124
|
+
if adapter_info[:execution_mode] == :async
|
|
125
|
+
base_score += 10 # Async generally better for I/O
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Engine-specific adjustments
|
|
129
|
+
case adapter_info[:engine_name]
|
|
130
|
+
when :typesense
|
|
131
|
+
base_score += 15 # Generally faster for simple queries
|
|
132
|
+
when :elasticsearch, :opensearch
|
|
133
|
+
base_score += 5 # Good for complex queries
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
estimated_score: [base_score, 0].max,
|
|
138
|
+
factors: {
|
|
139
|
+
complexity_penalty: complexity_penalty,
|
|
140
|
+
async_bonus: adapter_info[:execution_mode] == :async ? 10 : 0,
|
|
141
|
+
engine_factor: case adapter_info[:engine_name]
|
|
142
|
+
when :typesense then 15
|
|
143
|
+
when :elasticsearch, :opensearch then 5
|
|
144
|
+
else 0
|
|
145
|
+
end
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.analyze_cross_engine_compatibility(translations)
|
|
151
|
+
available_engines = translations.reject { |_, data| data.key?(:error) }
|
|
152
|
+
|
|
153
|
+
analysis = {
|
|
154
|
+
compatible_engines: available_engines.keys,
|
|
155
|
+
query_variations: {},
|
|
156
|
+
potential_issues: []
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Compare query structures across engines
|
|
160
|
+
queries = available_engines.transform_values { |data| data[:engine_query] }
|
|
161
|
+
|
|
162
|
+
if queries.values.uniq.size > 1
|
|
163
|
+
analysis[:query_variations] = queries
|
|
164
|
+
analysis[:potential_issues] << {
|
|
165
|
+
type: :query_structure_differences,
|
|
166
|
+
description: "Engines produce different query structures",
|
|
167
|
+
severity: :medium
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check for feature compatibility
|
|
172
|
+
features_by_engine = available_engines.transform_values do |data|
|
|
173
|
+
data[:adapter_info][:capabilities]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
common_features = features_by_engine.values.reduce(:&)
|
|
177
|
+
analysis[:common_features] = common_features
|
|
178
|
+
|
|
179
|
+
analysis
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.generate_recommendations(comparison)
|
|
183
|
+
recommendations = []
|
|
184
|
+
|
|
185
|
+
# Performance recommendations
|
|
186
|
+
best_performance = comparison[:engine_translations]
|
|
187
|
+
.reject { |_, data| data.key?(:error) }
|
|
188
|
+
.max_by { |_, data| data[:estimated_performance][:estimated_score] }
|
|
189
|
+
|
|
190
|
+
if best_performance
|
|
191
|
+
recommendations << {
|
|
192
|
+
type: :performance,
|
|
193
|
+
recommendation: "Consider using #{best_performance[0]} for optimal performance",
|
|
194
|
+
score: best_performance[1][:estimated_performance][:estimated_score]
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Compatibility recommendations
|
|
199
|
+
if comparison[:compatibility_analysis][:potential_issues].any?
|
|
200
|
+
recommendations << {
|
|
201
|
+
type: :compatibility,
|
|
202
|
+
recommendation: "Query may behave differently across engines",
|
|
203
|
+
issues: comparison[:compatibility_analysis][:potential_issues]
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
recommendations
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.visualize_as_tree(node, depth = 0)
|
|
211
|
+
indent = " " * depth
|
|
212
|
+
|
|
213
|
+
case node
|
|
214
|
+
when Hash
|
|
215
|
+
result = ""
|
|
216
|
+
node.each do |key, value|
|
|
217
|
+
result += "#{indent}#{key}:\n"
|
|
218
|
+
result += visualize_as_tree(value, depth + 1)
|
|
219
|
+
end
|
|
220
|
+
result
|
|
221
|
+
when Array
|
|
222
|
+
result = ""
|
|
223
|
+
node.each_with_index do |item, index|
|
|
224
|
+
result += "#{indent}[#{index}]:\n"
|
|
225
|
+
result += visualize_as_tree(item, depth + 1)
|
|
226
|
+
end
|
|
227
|
+
result
|
|
228
|
+
else
|
|
229
|
+
"#{indent}#{node}\n"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.generate_mermaid_diagram(ast_node)
|
|
234
|
+
diagram = "graph TD\n".dup
|
|
235
|
+
|
|
236
|
+
add_node = lambda do |diagram, node, parent_id, counter|
|
|
237
|
+
case node
|
|
238
|
+
when Hash
|
|
239
|
+
node.each do |key, value|
|
|
240
|
+
current_id = "N#{counter[:count]}"
|
|
241
|
+
counter[:count] += 1
|
|
242
|
+
diagram << " #{current_id}[#{key}]\n"
|
|
243
|
+
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
244
|
+
add_node.call(diagram, value, current_id, counter)
|
|
245
|
+
end
|
|
246
|
+
when Array
|
|
247
|
+
node.each_with_index do |item, index|
|
|
248
|
+
current_id = "N#{counter[:count]}"
|
|
249
|
+
counter[:count] += 1
|
|
250
|
+
diagram << " #{current_id}[Item #{index}]\n"
|
|
251
|
+
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
252
|
+
add_node.call(diagram, item, current_id, counter)
|
|
253
|
+
end
|
|
254
|
+
else
|
|
255
|
+
current_id = "N#{counter[:count]}"
|
|
256
|
+
counter[:count] += 1
|
|
257
|
+
diagram << " #{current_id}[#{node}]\n"
|
|
258
|
+
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
counter = { count: 0 }
|
|
263
|
+
root_id = "N#{counter[:count]}"
|
|
264
|
+
counter[:count] += 1
|
|
265
|
+
diagram << " #{root_id}[Root]\n"
|
|
266
|
+
|
|
267
|
+
add_node.call(diagram, ast_node.to_h, root_id, counter)
|
|
268
|
+
diagram
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def self.generate_flow_diagram(explanation)
|
|
272
|
+
# Create a sequence diagram showing the query execution flow
|
|
273
|
+
diagram = "sequenceDiagram\n".dup
|
|
274
|
+
diagram << " participant Client\n"
|
|
275
|
+
diagram << " participant Adapter\n"
|
|
276
|
+
diagram << " participant Engine\n"
|
|
277
|
+
|
|
278
|
+
explanation[:execution_plan].each do |step|
|
|
279
|
+
diagram << case step[:description]
|
|
280
|
+
when /validate/i
|
|
281
|
+
" Client->>Adapter: #{step[:description]}\n"
|
|
282
|
+
when /convert/i, /format/i
|
|
283
|
+
" Adapter->>Adapter: #{step[:description]}\n"
|
|
284
|
+
when /execute/i, /query/i
|
|
285
|
+
" Adapter->>Engine: #{step[:description]}\n"
|
|
286
|
+
else
|
|
287
|
+
" Engine->>Adapter: #{step[:description]}\n"
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
diagram << " Adapter->>Client: Return results\n"
|
|
292
|
+
diagram
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.format_performance_breakdown(performance)
|
|
296
|
+
performance.map do |metric, value|
|
|
297
|
+
{
|
|
298
|
+
metric: metric.to_s.humanize,
|
|
299
|
+
value: value,
|
|
300
|
+
unit: metric.to_s.end_with?("_ms") ? "milliseconds" : "unknown"
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def self.suggest_optimizations(explanation)
|
|
306
|
+
suggestions = []
|
|
307
|
+
|
|
308
|
+
# Check for slow AST conversion
|
|
309
|
+
if explanation[:performance][:ast_conversion_ms] > 1.0
|
|
310
|
+
suggestions << {
|
|
311
|
+
type: :performance,
|
|
312
|
+
area: :ast_conversion,
|
|
313
|
+
suggestion: "AST conversion is slow. Consider simplifying the query structure.",
|
|
314
|
+
impact: :medium
|
|
315
|
+
}
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
suggestions
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Helper methods
|
|
322
|
+
def self.extract_fields_from_ast(ast)
|
|
323
|
+
fields = []
|
|
324
|
+
extract_recursive(ast, fields)
|
|
325
|
+
fields.uniq
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def self.extract_recursive(node, fields)
|
|
329
|
+
case node
|
|
330
|
+
when Hash
|
|
331
|
+
fields << node["field"] if node["field"]
|
|
332
|
+
fields << node[:field] if node[:field]
|
|
333
|
+
node.each_value { |value| extract_recursive(value, fields) }
|
|
334
|
+
when Array
|
|
335
|
+
node.each { |item| extract_recursive(item, fields) }
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def self.extract_fields_from_query(_query)
|
|
340
|
+
# This would need to be engine-specific
|
|
341
|
+
# For now, return empty array
|
|
342
|
+
[]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def self.has_structural_differences?(ast, query)
|
|
346
|
+
# Simple heuristic - if the query has different top-level keys
|
|
347
|
+
ast_keys = ast.keys.sort
|
|
348
|
+
query_keys = query.keys.sort
|
|
349
|
+
ast_keys != query_keys
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def self.calculate_query_complexity(query)
|
|
353
|
+
complexity_counter = { count: 0 }
|
|
354
|
+
count_recursive(query, complexity_counter)
|
|
355
|
+
complexity_counter[:count]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def self.count_recursive(node, complexity)
|
|
359
|
+
case node
|
|
360
|
+
when Hash
|
|
361
|
+
complexity[:count] += node.size
|
|
362
|
+
node.each_value { |value| count_recursive(value, complexity) }
|
|
363
|
+
when Array
|
|
364
|
+
complexity[:count] += node.size
|
|
365
|
+
node.each { |item| count_recursive(item, complexity) }
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# New methods using your diagram/mermaid gems
|
|
370
|
+
def self.ast_to_mermaid_flowchart(ast_node)
|
|
371
|
+
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
372
|
+
|
|
373
|
+
diagram = Diagrams::FlowchartDiagram.new(version: "1.0")
|
|
374
|
+
|
|
375
|
+
# Convert AST structure to flowchart nodes and edges
|
|
376
|
+
add_ast_node_to_flowchart(diagram, ast_node, "root")
|
|
377
|
+
|
|
378
|
+
diagram.to_mermaid
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def self.ast_to_mermaid_class_diagram(ast_node)
|
|
382
|
+
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
383
|
+
|
|
384
|
+
diagram = Diagrams::ClassDiagram.new(version: "1.0")
|
|
385
|
+
|
|
386
|
+
# Create a class representation of the AST structure
|
|
387
|
+
root_class = Diagrams::Elements::ClassEntity.new(
|
|
388
|
+
name: ast_node.class.name.split("::").last,
|
|
389
|
+
attributes: ast_node.instance_variables.map do |var|
|
|
390
|
+
"#{var.to_s.delete('@')}: #{ast_node.instance_variable_get(var).class.name.split('::').last}"
|
|
391
|
+
end,
|
|
392
|
+
methods: ast_node.public_methods(false).map { |method| "+#{method}()" }
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
diagram.add_class(root_class)
|
|
396
|
+
|
|
397
|
+
# Add child nodes as related classes
|
|
398
|
+
add_ast_children_to_class_diagram(diagram, ast_node, root_class.name)
|
|
399
|
+
|
|
400
|
+
diagram.to_mermaid
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def self.adapter_capability_matrix_to_mermaid
|
|
404
|
+
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
405
|
+
|
|
406
|
+
# Create an ER diagram showing adapter capabilities
|
|
407
|
+
diagram = Diagrams::ERDiagram.new
|
|
408
|
+
|
|
409
|
+
# Add adapter entities
|
|
410
|
+
diagram.add_entity(
|
|
411
|
+
name: "ADAPTER",
|
|
412
|
+
attributes: [
|
|
413
|
+
{ type: "string", name: "type", keys: [:PK] },
|
|
414
|
+
{ type: "string", name: "execution_mode" },
|
|
415
|
+
{ type: "string", name: "engine_name" }
|
|
416
|
+
]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
diagram.add_entity(
|
|
420
|
+
name: "CAPABILITY",
|
|
421
|
+
attributes: [
|
|
422
|
+
{ type: "string", name: "name", keys: [:PK] },
|
|
423
|
+
{ type: "string", name: "description" }
|
|
424
|
+
]
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
diagram.add_entity(
|
|
428
|
+
name: "ADAPTER_CAPABILITY",
|
|
429
|
+
attributes: [
|
|
430
|
+
{ type: "string", name: "adapter_type", keys: %i[PK FK] },
|
|
431
|
+
{ type: "string", name: "capability_name", keys: %i[PK FK] }
|
|
432
|
+
]
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Add relationships
|
|
436
|
+
diagram.add_relationship(
|
|
437
|
+
entity1: "ADAPTER",
|
|
438
|
+
entity2: "ADAPTER_CAPABILITY",
|
|
439
|
+
cardinality1: :ONE_ONLY,
|
|
440
|
+
cardinality2: :ZERO_OR_MORE,
|
|
441
|
+
label: "has"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
diagram.add_relationship(
|
|
445
|
+
entity1: "CAPABILITY",
|
|
446
|
+
entity2: "ADAPTER_CAPABILITY",
|
|
447
|
+
cardinality1: :ONE_ONLY,
|
|
448
|
+
cardinality2: :ZERO_OR_MORE,
|
|
449
|
+
label: "provided by"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
diagram.to_mermaid
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def self.add_ast_node_to_flowchart(diagram, node, node_id)
|
|
456
|
+
# Add current node
|
|
457
|
+
flowchart_node = Diagrams::Elements::Node.new(
|
|
458
|
+
id: node_id,
|
|
459
|
+
label: node.class.name.split("::").last.to_s
|
|
460
|
+
)
|
|
461
|
+
diagram.add_node(flowchart_node)
|
|
462
|
+
|
|
463
|
+
# Add child nodes and connect them
|
|
464
|
+
return unless node.respond_to?(:instance_variables)
|
|
465
|
+
|
|
466
|
+
node.instance_variables.each_with_index do |var, _index|
|
|
467
|
+
child_value = node.instance_variable_get(var)
|
|
468
|
+
|
|
469
|
+
if child_value.is_a?(Noiseless::AST::Node)
|
|
470
|
+
child_id = "#{node_id}_#{var.to_s.delete('@')}"
|
|
471
|
+
add_ast_node_to_flowchart(diagram, child_value, child_id)
|
|
472
|
+
|
|
473
|
+
edge = Diagrams::Elements::Edge.new(
|
|
474
|
+
source_id: node_id,
|
|
475
|
+
target_id: child_id,
|
|
476
|
+
label: var.to_s.delete("@")
|
|
477
|
+
)
|
|
478
|
+
diagram.add_edge(edge)
|
|
479
|
+
elsif child_value.is_a?(Array) && child_value.any?(Noiseless::AST::Node)
|
|
480
|
+
child_value.each_with_index do |item, item_index|
|
|
481
|
+
next unless item.is_a?(Noiseless::AST::Node)
|
|
482
|
+
|
|
483
|
+
child_id = "#{node_id}_#{var.to_s.delete('@')}_#{item_index}"
|
|
484
|
+
add_ast_node_to_flowchart(diagram, item, child_id)
|
|
485
|
+
|
|
486
|
+
edge = Diagrams::Elements::Edge.new(
|
|
487
|
+
source_id: node_id,
|
|
488
|
+
target_id: child_id,
|
|
489
|
+
label: "#{var.to_s.delete('@')}[#{item_index}]"
|
|
490
|
+
)
|
|
491
|
+
diagram.add_edge(edge)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def self.add_ast_children_to_class_diagram(diagram, node, parent_class_name)
|
|
498
|
+
return unless node.respond_to?(:instance_variables)
|
|
499
|
+
|
|
500
|
+
node.instance_variables.each do |var|
|
|
501
|
+
child_value = node.instance_variable_get(var)
|
|
502
|
+
|
|
503
|
+
next unless child_value.is_a?(Noiseless::AST::Node)
|
|
504
|
+
|
|
505
|
+
child_class_name = child_value.class.name.split("::").last
|
|
506
|
+
|
|
507
|
+
# Add child class if not already added
|
|
508
|
+
unless diagram.classes.any? { |c| c.name == child_class_name }
|
|
509
|
+
child_class = Diagrams::Elements::ClassEntity.new(
|
|
510
|
+
name: child_class_name,
|
|
511
|
+
attributes: child_value.instance_variables.map do |cv|
|
|
512
|
+
"#{cv.to_s.delete('@')}: #{child_value.instance_variable_get(cv).class.name.split('::').last}"
|
|
513
|
+
end
|
|
514
|
+
)
|
|
515
|
+
diagram.add_class(child_class)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Add relationship
|
|
519
|
+
relationship = Diagrams::Elements::Relationship.new(
|
|
520
|
+
source_class_name: parent_class_name,
|
|
521
|
+
target_class_name: child_class_name,
|
|
522
|
+
type: "composition",
|
|
523
|
+
label: var.to_s.delete("@")
|
|
524
|
+
)
|
|
525
|
+
diagram.add_relationship(relationship)
|
|
526
|
+
|
|
527
|
+
# Recursively add children
|
|
528
|
+
add_ast_children_to_class_diagram(diagram, child_value, child_class_name)
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|