synthra 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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +40 -0
- data/.yardopts +18 -0
- data/ACCESS_DOCS.md +116 -0
- data/ADVANCED_FEATURES_SUMMARY.md +245 -0
- data/CHANGELOG.md +498 -0
- data/CODE_OF_CONDUCT.md +122 -0
- data/CONTRIBUTING.md +307 -0
- data/DOCUMENTATION.md +204 -0
- data/DOCUMENTATION_GUIDE.md +194 -0
- data/LICENSE +22 -0
- data/README.md +1458 -0
- data/Rakefile +128 -0
- data/benchmark/README.md +591 -0
- data/benchmark/batch_generation.rb +63 -0
- data/benchmark/engine_comparison.rb +295 -0
- data/benchmark/single_record.rb +108 -0
- data/benchmark/streaming.rb +74 -0
- data/docs/ACTIVERECORD_INFERENCE.md +524 -0
- data/docs/API_SERVER.md +278 -0
- data/docs/CONFIG_FILE.md +315 -0
- data/docs/DATA_CONTRACTS.md +312 -0
- data/docs/ENHANCED_REPL.md +304 -0
- data/docs/FACTORY_BOT.md +271 -0
- data/docs/GITHUB_ACTION.md +420 -0
- data/docs/GRAPHQL_EXPORT.md +303 -0
- data/docs/GRAPHQL_FEDERATION.md +651 -0
- data/docs/HOW_TO_GENERATE_DOCS.md +252 -0
- data/docs/LSP.md +189 -0
- data/docs/MIGRATION_GENERATOR.md +547 -0
- data/docs/MOCK_SERVER.md +280 -0
- data/docs/NATIVE_ENGINE.md +298 -0
- data/docs/OPENAPI_EXPORT.md +252 -0
- data/docs/PERFORMANCE_MODE.md +228 -0
- data/docs/PERSONAS.md +547 -0
- data/docs/PROPERTY_TESTING.md +284 -0
- data/docs/PROTOBUF_EXPORT.md +276 -0
- data/docs/QUALITY_METRICS.md +555 -0
- data/docs/QUICK_REFERENCE.md +88 -0
- data/docs/RAILS_ENGINE.md +404 -0
- data/docs/RAILS_INTEGRATION.md +454 -0
- data/docs/README.md +175 -0
- data/docs/REPL_QUICK_REFERENCE.md +149 -0
- data/docs/RUST_INTEGRATION_GUIDE.md +1376 -0
- data/docs/SCENARIOS.md +576 -0
- data/docs/SECURITY_TESTING.md +318 -0
- data/docs/SNAPSHOT_TESTING.md +308 -0
- data/docs/STRING_TEXT_TYPES_REFERENCE.md +512 -0
- data/docs/TERRAFORM_EXPORT.md +447 -0
- data/docs/TIME_TRAVEL.md +507 -0
- data/docs/USAGE_GUIDE.md +980 -0
- data/docs/WEBHOOK_SIMULATOR.md +579 -0
- data/docs/advanced_features.md +183 -0
- data/docs/api_reference.md +153 -0
- data/docs/behaviors.md +138 -0
- data/docs/best_practices.md +191 -0
- data/docs/dsl_reference.md +172 -0
- data/docs/examples.md +162 -0
- data/docs/generation_modes.md +125 -0
- data/docs/getting_started.md +92 -0
- data/docs/index.html +177 -0
- data/docs/strategic/CLOUD_REGISTRY_ARCHITECTURE.md +268 -0
- data/docs/strategic/SIMD_OPTIMIZATION_STRATEGY.md +153 -0
- data/docs/troubleshooting.md +215 -0
- data/docs/type_reference.md +224 -0
- data/examples/hostile_mode_demo.rb +64 -0
- data/exe/synthra +6 -0
- data/ext/synthra_native/Cargo.lock +972 -0
- data/ext/synthra_native/Cargo.toml +46 -0
- data/ext/synthra_native/extconf.rb +41 -0
- data/ext/synthra_native/src/generator.rs +397 -0
- data/ext/synthra_native/src/lib.rs +229 -0
- data/ext/synthra_native/src/types.rs +1485 -0
- data/lib/generators/synthra/install_generator.rb +86 -0
- data/lib/generators/synthra/templates/api_response.dsl +15 -0
- data/lib/generators/synthra/templates/fake_data.rake +116 -0
- data/lib/generators/synthra/templates/synthra.yml +31 -0
- data/lib/generators/synthra/templates/synthra_support.rb +19 -0
- data/lib/generators/synthra/templates/user.dsl +11 -0
- data/lib/synthra/activerecord_inference.rb +394 -0
- data/lib/synthra/api.rb +381 -0
- data/lib/synthra/api_server.rb +718 -0
- data/lib/synthra/behaviors/applicator.rb +165 -0
- data/lib/synthra/behaviors/base.rb +155 -0
- data/lib/synthra/behaviors/close_connection.rb +60 -0
- data/lib/synthra/behaviors/deprecated.rb +100 -0
- data/lib/synthra/behaviors/failure.rb +69 -0
- data/lib/synthra/behaviors/latency.rb +103 -0
- data/lib/synthra/behaviors/partial_data.rb +83 -0
- data/lib/synthra/behaviors/randomize_order.rb +74 -0
- data/lib/synthra/behaviors/registry.rb +238 -0
- data/lib/synthra/behaviors/simulate_error.rb +79 -0
- data/lib/synthra/cli/commands/base.rb +105 -0
- data/lib/synthra/cli/commands/diff.rb +227 -0
- data/lib/synthra/cli/commands/docs.rb +67 -0
- data/lib/synthra/cli/commands/export.rb +150 -0
- data/lib/synthra/cli/commands/generate.rb +72 -0
- data/lib/synthra/cli/commands/import.rb +80 -0
- data/lib/synthra/cli/commands/lint.rb +53 -0
- data/lib/synthra/cli/commands/live.rb +57 -0
- data/lib/synthra/cli/commands/seed.rb +99 -0
- data/lib/synthra/cli/commands/validate.rb +28 -0
- data/lib/synthra/cli.rb +2471 -0
- data/lib/synthra/config_file.rb +129 -0
- data/lib/synthra/configuration.rb +281 -0
- data/lib/synthra/contracts_registry.rb +408 -0
- data/lib/synthra/database_seeder.rb +268 -0
- data/lib/synthra/deterministic_ids.rb +218 -0
- data/lib/synthra/documentation_generator.rb +414 -0
- data/lib/synthra/engine.rb +251 -0
- data/lib/synthra/errors.rb +1169 -0
- data/lib/synthra/export/base.rb +85 -0
- data/lib/synthra/export/csv.rb +101 -0
- data/lib/synthra/export/graphql.rb +266 -0
- data/lib/synthra/export/graphql_federation.rb +377 -0
- data/lib/synthra/export/graphviz.rb +258 -0
- data/lib/synthra/export/javascript.rb +327 -0
- data/lib/synthra/export/json_data.rb +61 -0
- data/lib/synthra/export/json_schema.rb +290 -0
- data/lib/synthra/export/openapi.rb +514 -0
- data/lib/synthra/export/protobuf.rb +483 -0
- data/lib/synthra/export/python.rb +560 -0
- data/lib/synthra/export/sql.rb +381 -0
- data/lib/synthra/export/sql_insert.rb +152 -0
- data/lib/synthra/export/terraform.rb +501 -0
- data/lib/synthra/export/type_mapping.rb +389 -0
- data/lib/synthra/export/typescript.rb +300 -0
- data/lib/synthra/export/xml_data.rb +104 -0
- data/lib/synthra/export/yaml_data.rb +56 -0
- data/lib/synthra/export.rb +404 -0
- data/lib/synthra/factory_bot_integration.rb +157 -0
- data/lib/synthra/field.rb +440 -0
- data/lib/synthra/functions/registry.rb +104 -0
- data/lib/synthra/generator/context.rb +372 -0
- data/lib/synthra/generator/engine.rb +336 -0
- data/lib/synthra/generator/faker_adapter.rb +425 -0
- data/lib/synthra/generator/modes.rb +444 -0
- data/lib/synthra/generator/resolver.rb +256 -0
- data/lib/synthra/generator/rng.rb +273 -0
- data/lib/synthra/generator/streamer.rb +63 -0
- data/lib/synthra/generator/uniqueness.rb +118 -0
- data/lib/synthra/initializer.rb +97 -0
- data/lib/synthra/limits.rb +279 -0
- data/lib/synthra/live_preview.rb +518 -0
- data/lib/synthra/loader_config.rb +78 -0
- data/lib/synthra/lsp/server.rb +689 -0
- data/lib/synthra/migration_generator.rb +301 -0
- data/lib/synthra/mixin.rb +174 -0
- data/lib/synthra/mock_server.rb +458 -0
- data/lib/synthra/native_engine.rb +304 -0
- data/lib/synthra/openapi_importer.rb +228 -0
- data/lib/synthra/output/json_formatter.rb +40 -0
- data/lib/synthra/output/ndjson_formatter.rb +55 -0
- data/lib/synthra/parser/ast.rb +1287 -0
- data/lib/synthra/parser/lexer.rb +1152 -0
- data/lib/synthra/parser/parser.rb +1664 -0
- data/lib/synthra/parser/tokens.rb +461 -0
- data/lib/synthra/performance_mode.rb +364 -0
- data/lib/synthra/personas.rb +397 -0
- data/lib/synthra/property_testing.rb +172 -0
- data/lib/synthra/quality_metrics.rb +405 -0
- data/lib/synthra/rails_test_helper.rb +248 -0
- data/lib/synthra/registry.rb +533 -0
- data/lib/synthra/relationships.rb +193 -0
- data/lib/synthra/repl/enhanced_repl.rb +607 -0
- data/lib/synthra/repl/formatter.rb +191 -0
- data/lib/synthra/scenarios.rb +423 -0
- data/lib/synthra/schema.rb +605 -0
- data/lib/synthra/schema_inheritance.rb +104 -0
- data/lib/synthra/schema_versioning.rb +212 -0
- data/lib/synthra/snapshot_testing.rb +199 -0
- data/lib/synthra/time_travel.rb +338 -0
- data/lib/synthra/type_definitions.rb +274 -0
- data/lib/synthra/types/address_location/addresses.rb +125 -0
- data/lib/synthra/types/address_location/airports.rb +200 -0
- data/lib/synthra/types/address_location/banks_hospitals.rb +207 -0
- data/lib/synthra/types/address_location/locations.rb +406 -0
- data/lib/synthra/types/base.rb +48 -0
- data/lib/synthra/types/commerce_products/commerce.rb +119 -0
- data/lib/synthra/types/commerce_products/companies.rb +71 -0
- data/lib/synthra/types/commerce_products/construction.rb +126 -0
- data/lib/synthra/types/commerce_products/products.rb +205 -0
- data/lib/synthra/types/core/collections.rb +513 -0
- data/lib/synthra/types/core/defaults.rb +76 -0
- data/lib/synthra/types/core/enums.rb +102 -0
- data/lib/synthra/types/core/identifiers.rb +445 -0
- data/lib/synthra/types/core/primitives.rb +586 -0
- data/lib/synthra/types/core/references.rb +466 -0
- data/lib/synthra/types/core/sequences.rb +304 -0
- data/lib/synthra/types/crypto/crypto.rb +162 -0
- data/lib/synthra/types/date_time/dates.rb +387 -0
- data/lib/synthra/types/finance_banking/banking.rb +424 -0
- data/lib/synthra/types/finance_banking/credit_cards.rb +24 -0
- data/lib/synthra/types/finance_banking/identifiers.rb +93 -0
- data/lib/synthra/types/formula.rb +184 -0
- data/lib/synthra/types/health_medical/medical.rb +150 -0
- data/lib/synthra/types/hostile_payloads.rb +122 -0
- data/lib/synthra/types/json_array.rb +51 -0
- data/lib/synthra/types/media_entertainment/media.rb +62 -0
- data/lib/synthra/types/naughty_string.rb +30 -0
- data/lib/synthra/types/personal_names/chinese.rb +41 -0
- data/lib/synthra/types/personal_names/identifiers.rb +187 -0
- data/lib/synthra/types/personal_names/names.rb +246 -0
- data/lib/synthra/types/personal_names/national_id.rb +89 -0
- data/lib/synthra/types/personal_names/titles_suffixes.rb +41 -0
- data/lib/synthra/types/regex.rb +248 -0
- data/lib/synthra/types/registry.rb +182 -0
- data/lib/synthra/types/repeating_element.rb +50 -0
- data/lib/synthra/types/scenario.rb +29 -0
- data/lib/synthra/types/technology_internet/apps.rb +67 -0
- data/lib/synthra/types/technology_internet/communication.rb +162 -0
- data/lib/synthra/types/technology_internet/devices.rb +80 -0
- data/lib/synthra/types/technology_internet/formats.rb +139 -0
- data/lib/synthra/types/technology_internet/networking.rb +143 -0
- data/lib/synthra/types/template.rb +92 -0
- data/lib/synthra/types/text_content/business.rb +128 -0
- data/lib/synthra/types/text_content/colors.rb +70 -0
- data/lib/synthra/types/text_content/misc.rb +237 -0
- data/lib/synthra/types/text_content/security.rb +130 -0
- data/lib/synthra/types/text_content/text_generation.rb +532 -0
- data/lib/synthra/types/travel/travel.rb +150 -0
- data/lib/synthra/utils/string_distance.rb +133 -0
- data/lib/synthra/validator/dsl_validator.rb +339 -0
- data/lib/synthra/validator/path_validator.rb +615 -0
- data/lib/synthra/version.rb +35 -0
- data/lib/synthra/webhook_simulator.rb +341 -0
- data/lib/synthra.rb +259 -0
- data/schemas/address.dsl +16 -0
- data/schemas/api_response.dsl +8 -0
- data/schemas/error_payload.dsl +9 -0
- data/schemas/order.dsl +10 -0
- data/schemas/order_item.dsl +8 -0
- data/schemas/payment.dsl +11 -0
- data/schemas/social_post.dsl +13 -0
- data/schemas/user.dsl +10 -0
- data/scripts/batch_fix_all.rb +96 -0
- data/scripts/delete_old_files.rb +32 -0
- data/scripts/fix_all_grouped_files.rb +137 -0
- data/scripts/fix_all_indentation.rb +48 -0
- data/scripts/fix_all_syntax.rb +124 -0
- data/scripts/fix_grouped_files.rb +184 -0
- data/scripts/fix_syntax_errors.rb +108 -0
- data/scripts/group_domain_types.rb +112 -0
- data/scripts/group_domain_types_fixed.rb +150 -0
- data/scripts/merge_domains_to_one_file.rb +68 -0
- data/scripts/move_existing_types.rb +130 -0
- data/scripts/split_grouped_types.rb +142 -0
- data/tech_docs/README.md +134 -0
- data/tech_docs/advanced/streaming.md +405 -0
- data/tech_docs/advanced/thread_safety.md +253 -0
- data/tech_docs/api/overview.md +485 -0
- data/tech_docs/appendices/type_chart.md +193 -0
- data/tech_docs/basic_concepts.md +383 -0
- data/tech_docs/behaviors/overview.md +346 -0
- data/tech_docs/dsl/complex_types.md +790 -0
- data/tech_docs/dsl/core_types.md +464 -0
- data/tech_docs/dsl/datetime_types.md +325 -0
- data/tech_docs/dsl/field_modifiers.md +414 -0
- data/tech_docs/dsl/grammar.md +431 -0
- data/tech_docs/dsl/schema_definition.md +399 -0
- data/tech_docs/export/README.md +276 -0
- data/tech_docs/installation.md +273 -0
- data/tech_docs/integration/ci_cd.md +707 -0
- data/tech_docs/integration/ci_cd_guide.md +579 -0
- data/tech_docs/integration/factory_bot.md +485 -0
- data/tech_docs/integration/rails.md +630 -0
- data/tech_docs/integration/rspec.md +449 -0
- data/tech_docs/modes/overview.md +350 -0
- data/tech_docs/performance/NATIVE_RUST_EXTENSION.md +1270 -0
- data/tech_docs/performance/OPTIMIZATION_GUIDE.md +901 -0
- data/tech_docs/quick_start.md +256 -0
- data/tech_docs/templates/README.md +805 -0
- data/tech_docs/tutorials/advanced.md +371 -0
- data/tech_docs/tutorials/getting_started.md +189 -0
- data/tech_docs/tutorials/intermediate.md +231 -0
- data/tech_docs/tutorials/template_gallery.md +569 -0
- data/tech_example/01_basic_usage.rb +238 -0
- data/tech_example/02_types_demo.rb +336 -0
- data/tech_example/04_cli_usage.md +429 -0
- data/tech_example/05_database_seeding.rb +283 -0
- data/tech_example/07_rspec_integration.rb +359 -0
- data/tech_example/10_custom_types.rb +387 -0
- data/tech_example/12_behaviors.rb +372 -0
- data/tech_example/13_twitter_dm_example.rb +120 -0
- data/tech_example/14_exact_json_structure.rb +253 -0
- data/tech_example/16_lsp_server.rb +56 -0
- data/tech_example/17_property_testing.rb +92 -0
- data/tech_example/18_enhanced_repl.rb +120 -0
- data/tech_example/19_security_fuzzing.rb +145 -0
- data/tech_example/NEW_FEATURES.md +169 -0
- data/tech_example/README.md +118 -0
- data/tech_example/schemas/api_response.dsl +123 -0
- data/tech_example/schemas/ecommerce.dsl +121 -0
- data/tech_example/schemas/twitter_dm.dsl +109 -0
- data/tech_example/schemas/user.dsl +36 -0
- data/vscode-extension/README.md +246 -0
- data/vscode-extension/language-configuration.json +31 -0
- data/vscode-extension/package.json +55 -0
- data/vscode-extension/snippets/fakedatadsl.json +198 -0
- data/vscode-extension/syntaxes/fakedatadsl.tmLanguage.json +128 -0
- metadata +478 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Synthra Enhanced REPL with Visual Inspection and Debugging
|
|
5
|
+
# =============================================================================
|
|
6
|
+
#
|
|
7
|
+
# Provides enhanced REPL features:
|
|
8
|
+
# - Visual data inspection (table view, JSON pretty-print)
|
|
9
|
+
# - Step-through debugging (pause after each field, show Context state)
|
|
10
|
+
# - Better formatting and display
|
|
11
|
+
#
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
module Synthra
|
|
15
|
+
module REPL
|
|
16
|
+
# Visual formatter for REPL output
|
|
17
|
+
class Formatter
|
|
18
|
+
# Format data as a table
|
|
19
|
+
#
|
|
20
|
+
# @param data [Hash, Array] data to format
|
|
21
|
+
# @return [String] formatted table string
|
|
22
|
+
#
|
|
23
|
+
def self.table(data)
|
|
24
|
+
case data
|
|
25
|
+
when Hash
|
|
26
|
+
format_hash_table(data)
|
|
27
|
+
when Array
|
|
28
|
+
format_array_table(data)
|
|
29
|
+
else
|
|
30
|
+
data.inspect
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format hash as a table
|
|
35
|
+
#
|
|
36
|
+
# @param hash [Hash] hash to format
|
|
37
|
+
# @return [String] formatted table
|
|
38
|
+
#
|
|
39
|
+
def self.format_hash_table(hash)
|
|
40
|
+
return "{}" if hash.empty?
|
|
41
|
+
|
|
42
|
+
max_key_width = hash.keys.map { |k| k.to_s.length }.max
|
|
43
|
+
max_key_width = [max_key_width, 20].min # Cap at 20 chars
|
|
44
|
+
max_value_width = 60
|
|
45
|
+
|
|
46
|
+
lines = []
|
|
47
|
+
hash.each do |key, value|
|
|
48
|
+
key_str = key.to_s.ljust(max_key_width)
|
|
49
|
+
value_str = format_value(value, max_value_width)
|
|
50
|
+
lines << " #{key_str} │ #{value_str}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
header = " #{'─' * max_key_width}─┼─#{'─' * max_value_width}"
|
|
54
|
+
[header, *lines].join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Format array as a table
|
|
58
|
+
#
|
|
59
|
+
# @param array [Array] array to format
|
|
60
|
+
# @return [String] formatted table
|
|
61
|
+
#
|
|
62
|
+
def self.format_array_table(array)
|
|
63
|
+
return "[]" if array.empty?
|
|
64
|
+
|
|
65
|
+
# If array of hashes, show as table
|
|
66
|
+
if array.first.is_a?(Hash)
|
|
67
|
+
keys = array.first.keys
|
|
68
|
+
max_widths = keys.map { |k| [k.to_s.length, array.map { |h| format_value(h[k], 100).length }.max].max }
|
|
69
|
+
max_widths = max_widths.map { |w| [w, 30].min } # Cap column widths
|
|
70
|
+
|
|
71
|
+
# Header
|
|
72
|
+
header = " " + keys.map.with_index { |k, i| k.to_s.ljust(max_widths[i]) }.join(" │ ")
|
|
73
|
+
separator = " " + max_widths.map { |w| "─" * w }.join("─┼─")
|
|
74
|
+
|
|
75
|
+
rows = array.map do |hash|
|
|
76
|
+
" " + keys.map.with_index { |k, i| format_value(hash[k], max_widths[i]).ljust(max_widths[i]) }.join(" │ ")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
[header, separator, *rows].join("\n")
|
|
80
|
+
else
|
|
81
|
+
array.map.with_index { |item, i| " [#{i}] #{format_value(item, 60)}" }.join("\n")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Format a single value
|
|
86
|
+
#
|
|
87
|
+
# @param value [Object] value to format
|
|
88
|
+
# @param max_width [Integer] maximum width
|
|
89
|
+
# @return [String] formatted value
|
|
90
|
+
#
|
|
91
|
+
def self.format_value(value, max_width = 60)
|
|
92
|
+
case value
|
|
93
|
+
when nil
|
|
94
|
+
"nil"
|
|
95
|
+
when String
|
|
96
|
+
if value.length > max_width
|
|
97
|
+
"#{value[0, max_width - 3]}..."
|
|
98
|
+
else
|
|
99
|
+
value.inspect
|
|
100
|
+
end
|
|
101
|
+
when Hash
|
|
102
|
+
if value.empty?
|
|
103
|
+
"{}"
|
|
104
|
+
else
|
|
105
|
+
"{#{value.keys.length} keys}"
|
|
106
|
+
end
|
|
107
|
+
when Array
|
|
108
|
+
if value.empty?
|
|
109
|
+
"[]"
|
|
110
|
+
else
|
|
111
|
+
"[#{value.length} items]"
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
str = value.inspect
|
|
115
|
+
str.length > max_width ? "#{str[0, max_width - 3]}..." : str
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Format context state for debugging
|
|
120
|
+
#
|
|
121
|
+
# @param context [Context] context to format
|
|
122
|
+
# @return [String] formatted context state
|
|
123
|
+
#
|
|
124
|
+
def self.format_context(context)
|
|
125
|
+
lines = []
|
|
126
|
+
lines << " Context State:"
|
|
127
|
+
lines << " ──────────────"
|
|
128
|
+
lines << " Depth: #{context.depth}"
|
|
129
|
+
lines << " Registry: #{context.registry ? 'available' : 'none'}"
|
|
130
|
+
lines << " Parent Context: #{context.parent_context ? 'yes' : 'no'}"
|
|
131
|
+
lines << " Shared Context: #{context.shared_context ? "#{context.shared_context.size} entries" : 'none'}"
|
|
132
|
+
lines << ""
|
|
133
|
+
lines << " Generated Fields:"
|
|
134
|
+
lines << " ────────────────"
|
|
135
|
+
|
|
136
|
+
if context.empty?
|
|
137
|
+
lines << " (none yet)"
|
|
138
|
+
else
|
|
139
|
+
context.each do |key, value|
|
|
140
|
+
value_str = format_value(value, 50)
|
|
141
|
+
lines << " #{key.to_s.ljust(20)} = #{value_str}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
lines.join("\n")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Debug-enabled engine wrapper
|
|
150
|
+
class DebugEngine
|
|
151
|
+
def initialize(schema, registry: nil, mode: :random, seed: nil)
|
|
152
|
+
@schema = schema
|
|
153
|
+
@registry = registry
|
|
154
|
+
@mode = mode
|
|
155
|
+
@seed = seed
|
|
156
|
+
@debug_callbacks = []
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Add debug callback
|
|
160
|
+
#
|
|
161
|
+
# @yield [schema_name, field_name, value, duration_ms] callback block
|
|
162
|
+
# @note The callback receives 4 arguments matching the engine's on_field_generated signature
|
|
163
|
+
#
|
|
164
|
+
def on_field_generated(&block)
|
|
165
|
+
@debug_callbacks << block
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Generate with debugging
|
|
169
|
+
#
|
|
170
|
+
# @return [Hash] generated record
|
|
171
|
+
#
|
|
172
|
+
def generate
|
|
173
|
+
# Create a custom configuration that triggers callbacks
|
|
174
|
+
original_callback = Synthra.configuration.on_field_generated
|
|
175
|
+
|
|
176
|
+
# The engine calls on_field_generated with 4 args: schema_name, field_name, value, duration_ms
|
|
177
|
+
Synthra.configuration.on_field_generated = lambda do |schema_name, field_name, value, duration_ms|
|
|
178
|
+
original_callback&.call(schema_name, field_name, value, duration_ms)
|
|
179
|
+
@debug_callbacks.each { |cb| cb.call(schema_name, field_name, value, duration_ms) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
begin
|
|
183
|
+
engine = Generator::Engine.new(@schema, registry: @registry)
|
|
184
|
+
engine.generate(seed: @seed, mode: @mode)
|
|
185
|
+
ensure
|
|
186
|
+
Synthra.configuration.on_field_generated = original_callback
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Scenario-based Test Data Fixtures
|
|
5
|
+
#
|
|
6
|
+
# Define complex, interconnected test data scenarios that can be
|
|
7
|
+
# easily reused across tests. Think "Fixtures 2.0" - dynamic,
|
|
8
|
+
# relationship-aware, and DRY.
|
|
9
|
+
#
|
|
10
|
+
# @example Define a scenario
|
|
11
|
+
# Synthra::Scenarios.define(:e_commerce_checkout) do
|
|
12
|
+
# let(:buyer) { User(role: "customer", verified: true) }
|
|
13
|
+
# let(:seller) { User(role: "merchant") }
|
|
14
|
+
# let(:laptop) { Product(seller: seller, price: 999.99) }
|
|
15
|
+
# let(:cart) { ShoppingCart(user: buyer, items: [laptop]) }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Use in tests
|
|
19
|
+
# RSpec.describe "Checkout" do
|
|
20
|
+
# include_scenario :e_commerce_checkout
|
|
21
|
+
#
|
|
22
|
+
# it "processes payment" do
|
|
23
|
+
# expect(buyer.email).to be_present
|
|
24
|
+
# expect(cart.items).to include(laptop)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
module Scenarios
|
|
29
|
+
class << self
|
|
30
|
+
# Registry of defined scenarios
|
|
31
|
+
def registry
|
|
32
|
+
@registry ||= {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Define a new scenario
|
|
36
|
+
#
|
|
37
|
+
# @param name [Symbol] scenario name
|
|
38
|
+
# @param options [Hash] scenario options
|
|
39
|
+
# @option options [Symbol] :extends parent scenario to inherit from
|
|
40
|
+
# @yield scenario definition block
|
|
41
|
+
# @return [Scenario] the defined scenario
|
|
42
|
+
#
|
|
43
|
+
def define(name, **options, &block)
|
|
44
|
+
scenario = Scenario.new(name, **options)
|
|
45
|
+
scenario.instance_eval(&block) if block
|
|
46
|
+
registry[name] = scenario
|
|
47
|
+
scenario
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get a scenario by name
|
|
51
|
+
#
|
|
52
|
+
# @param name [Symbol] scenario name
|
|
53
|
+
# @return [Scenario, nil] the scenario or nil
|
|
54
|
+
#
|
|
55
|
+
def [](name)
|
|
56
|
+
registry[name]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# List all defined scenarios
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<Symbol>] scenario names
|
|
62
|
+
#
|
|
63
|
+
def list
|
|
64
|
+
registry.keys
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Load scenarios from a file
|
|
68
|
+
#
|
|
69
|
+
# @param path [String] path to scenario file
|
|
70
|
+
#
|
|
71
|
+
def load_file(path)
|
|
72
|
+
content = File.read(path)
|
|
73
|
+
instance_eval(content, path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Load all scenarios from a directory
|
|
77
|
+
#
|
|
78
|
+
# @param dir [String] directory path
|
|
79
|
+
#
|
|
80
|
+
def load_dir(dir)
|
|
81
|
+
Dir.glob(File.join(dir, "**/*.rb")).each do |file|
|
|
82
|
+
load_file(file)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Clear all scenarios
|
|
87
|
+
def reset!
|
|
88
|
+
@registry = {}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A scenario definition
|
|
93
|
+
class Scenario
|
|
94
|
+
attr_reader :name, :options, :fixtures, :dependencies
|
|
95
|
+
|
|
96
|
+
def initialize(name, **options)
|
|
97
|
+
@name = name
|
|
98
|
+
@options = options
|
|
99
|
+
@fixtures = {}
|
|
100
|
+
@dependencies = []
|
|
101
|
+
@setup_blocks = []
|
|
102
|
+
@teardown_blocks = []
|
|
103
|
+
|
|
104
|
+
# Inherit from parent scenario
|
|
105
|
+
if options[:extends]
|
|
106
|
+
parent = Scenarios[options[:extends]]
|
|
107
|
+
raise ArgumentError, "Unknown parent scenario: #{options[:extends]}" unless parent
|
|
108
|
+
|
|
109
|
+
@fixtures = parent.fixtures.dup
|
|
110
|
+
@dependencies = parent.dependencies.dup
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Define a named fixture
|
|
115
|
+
#
|
|
116
|
+
# @param name [Symbol] fixture name
|
|
117
|
+
# @param schema [String, Symbol] schema name (optional)
|
|
118
|
+
# @param options [Hash] generation options
|
|
119
|
+
# @yield optional customization block
|
|
120
|
+
# @return [FixtureDefinition] the fixture definition
|
|
121
|
+
#
|
|
122
|
+
def let(name, schema = nil, **options, &block)
|
|
123
|
+
@fixtures[name] = FixtureDefinition.new(name, schema, **options, &block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Define a lazy fixture (evaluated on first access)
|
|
127
|
+
#
|
|
128
|
+
# @param name [Symbol] fixture name
|
|
129
|
+
# @yield block that returns the fixture value
|
|
130
|
+
#
|
|
131
|
+
def let!(name, &block)
|
|
132
|
+
@fixtures[name] = LazyFixture.new(name, &block)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Define a dependent fixture
|
|
136
|
+
#
|
|
137
|
+
# @param schema [String, Symbol] schema to generate
|
|
138
|
+
# @param name [Symbol] fixture name
|
|
139
|
+
# @param options [Hash] generation options with references
|
|
140
|
+
#
|
|
141
|
+
def create(schema, name = nil, **options)
|
|
142
|
+
name ||= schema.to_s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
143
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
144
|
+
.downcase
|
|
145
|
+
.to_sym
|
|
146
|
+
@fixtures[name] = FixtureDefinition.new(name, schema, **options)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Declare a dependency on another scenario
|
|
150
|
+
#
|
|
151
|
+
# @param scenario_name [Symbol] name of scenario to include
|
|
152
|
+
#
|
|
153
|
+
def depends_on(scenario_name)
|
|
154
|
+
@dependencies << scenario_name
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Add setup block
|
|
158
|
+
def setup(&block)
|
|
159
|
+
@setup_blocks << block
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Add teardown block
|
|
163
|
+
def teardown(&block)
|
|
164
|
+
@teardown_blocks << block
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Build the scenario context
|
|
168
|
+
#
|
|
169
|
+
# @param seed [Integer] random seed for determinism
|
|
170
|
+
# @return [ScenarioContext] built context with all fixtures
|
|
171
|
+
#
|
|
172
|
+
def build(seed: nil)
|
|
173
|
+
context = ScenarioContext.new(self, seed: seed)
|
|
174
|
+
|
|
175
|
+
# Load dependencies first
|
|
176
|
+
@dependencies.each do |dep_name|
|
|
177
|
+
dep = Scenarios[dep_name]
|
|
178
|
+
raise ArgumentError, "Unknown dependency: #{dep_name}" unless dep
|
|
179
|
+
|
|
180
|
+
dep_context = dep.build(seed: seed)
|
|
181
|
+
context.merge!(dep_context)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Run setup blocks
|
|
185
|
+
@setup_blocks.each { |block| context.instance_eval(&block) }
|
|
186
|
+
|
|
187
|
+
# Build fixtures in dependency order
|
|
188
|
+
build_order = resolve_build_order
|
|
189
|
+
build_order.each do |fixture_name|
|
|
190
|
+
context.build_fixture(fixture_name)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
context
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def resolve_build_order
|
|
199
|
+
# Simple topological sort based on references
|
|
200
|
+
# For now, just return fixtures in definition order
|
|
201
|
+
@fixtures.keys
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# A fixture definition
|
|
206
|
+
class FixtureDefinition
|
|
207
|
+
attr_reader :name, :schema, :options, :block
|
|
208
|
+
|
|
209
|
+
def initialize(name, schema = nil, **options, &block)
|
|
210
|
+
@name = name
|
|
211
|
+
@schema = schema
|
|
212
|
+
@options = options
|
|
213
|
+
@block = block
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build(context)
|
|
217
|
+
# Resolve references in options
|
|
218
|
+
resolved_options = resolve_references(context)
|
|
219
|
+
|
|
220
|
+
# Generate base data
|
|
221
|
+
if @schema
|
|
222
|
+
schema_obj = Synthra.registry.schema(@schema.to_s)
|
|
223
|
+
# :nocov: registry.schema raises KeyError for a missing schema, so this guard is unreachable
|
|
224
|
+
raise ArgumentError, "Unknown schema: #{@schema}" unless schema_obj
|
|
225
|
+
# :nocov:
|
|
226
|
+
|
|
227
|
+
data = schema_obj.generate(
|
|
228
|
+
seed: context.seed,
|
|
229
|
+
overrides: resolved_options
|
|
230
|
+
)
|
|
231
|
+
else
|
|
232
|
+
data = resolved_options
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Apply customization block
|
|
236
|
+
data = @block.call(data, context) if @block
|
|
237
|
+
|
|
238
|
+
data
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def resolve_references(context)
|
|
244
|
+
resolved = {}
|
|
245
|
+
|
|
246
|
+
@options.each do |key, value|
|
|
247
|
+
resolved[key.to_s] = resolve_value(value, context)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
resolved
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def resolve_value(value, context)
|
|
254
|
+
case value
|
|
255
|
+
when Symbol
|
|
256
|
+
# Reference to another fixture
|
|
257
|
+
context[value] || value.to_s
|
|
258
|
+
when Proc
|
|
259
|
+
value.call(context)
|
|
260
|
+
when Hash
|
|
261
|
+
value.transform_values { |v| resolve_value(v, context) }
|
|
262
|
+
when Array
|
|
263
|
+
value.map { |v| resolve_value(v, context) }
|
|
264
|
+
else
|
|
265
|
+
value
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# A lazy fixture that evaluates on first access
|
|
271
|
+
class LazyFixture
|
|
272
|
+
attr_reader :name, :block
|
|
273
|
+
|
|
274
|
+
def initialize(name, &block)
|
|
275
|
+
@name = name
|
|
276
|
+
@block = block
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build(context)
|
|
280
|
+
context.instance_eval(&@block)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Runtime context for a built scenario
|
|
285
|
+
class ScenarioContext
|
|
286
|
+
attr_reader :scenario, :seed, :data
|
|
287
|
+
|
|
288
|
+
def initialize(scenario, seed: nil)
|
|
289
|
+
@scenario = scenario
|
|
290
|
+
@seed = seed || Random.new_seed
|
|
291
|
+
@data = {}
|
|
292
|
+
@built = {}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Access a fixture by name
|
|
296
|
+
#
|
|
297
|
+
# @param name [Symbol] fixture name
|
|
298
|
+
# @return [Object] fixture value
|
|
299
|
+
#
|
|
300
|
+
def [](name)
|
|
301
|
+
@data[name]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Set a fixture value
|
|
305
|
+
def []=(name, value)
|
|
306
|
+
@data[name] = value
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Build a specific fixture
|
|
310
|
+
def build_fixture(name)
|
|
311
|
+
return @data[name] if @built[name]
|
|
312
|
+
|
|
313
|
+
fixture_def = @scenario.fixtures[name]
|
|
314
|
+
return nil unless fixture_def
|
|
315
|
+
|
|
316
|
+
@data[name] = fixture_def.build(self)
|
|
317
|
+
@built[name] = true
|
|
318
|
+
@data[name]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Merge another context
|
|
322
|
+
def merge!(other_context)
|
|
323
|
+
@data.merge!(other_context.data)
|
|
324
|
+
@built.merge!(other_context.instance_variable_get(:@built))
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Convert to hash
|
|
328
|
+
def to_h
|
|
329
|
+
@data.dup
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Method missing for fixture access
|
|
333
|
+
def method_missing(name, *args)
|
|
334
|
+
if @data.key?(name)
|
|
335
|
+
@data[name]
|
|
336
|
+
elsif @scenario.fixtures.key?(name)
|
|
337
|
+
build_fixture(name)
|
|
338
|
+
else
|
|
339
|
+
super
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def respond_to_missing?(name, include_private = false)
|
|
344
|
+
@data.key?(name) || @scenario.fixtures.key?(name) || super
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# RSpec integration
|
|
349
|
+
module RSpecHelpers
|
|
350
|
+
# Include a scenario in a test
|
|
351
|
+
#
|
|
352
|
+
# @param scenario_name [Symbol] name of scenario to include
|
|
353
|
+
# @param options [Hash] options
|
|
354
|
+
# @option options [Integer] :seed random seed
|
|
355
|
+
#
|
|
356
|
+
def include_scenario(scenario_name, **options)
|
|
357
|
+
scenario = Scenarios[scenario_name]
|
|
358
|
+
raise ArgumentError, "Unknown scenario: #{scenario_name}" unless scenario
|
|
359
|
+
|
|
360
|
+
let(:scenario_context) { scenario.build(**options) }
|
|
361
|
+
|
|
362
|
+
# Define let helpers for each fixture
|
|
363
|
+
scenario.fixtures.keys.each do |fixture_name|
|
|
364
|
+
let(fixture_name) { scenario_context[fixture_name] }
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Use scenario data in a before block
|
|
369
|
+
def use_scenario(scenario_name, **options)
|
|
370
|
+
before(:each) do
|
|
371
|
+
scenario = Scenarios[scenario_name]
|
|
372
|
+
@scenario_context = scenario.build(**options)
|
|
373
|
+
|
|
374
|
+
# Make fixtures available as instance variables
|
|
375
|
+
@scenario_context.data.each do |name, value|
|
|
376
|
+
instance_variable_set("@#{name}", value)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Minitest integration
|
|
383
|
+
module MinitestHelpers
|
|
384
|
+
def self.included(base)
|
|
385
|
+
base.extend(ClassMethods)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
module ClassMethods
|
|
389
|
+
def use_scenario(scenario_name, **options)
|
|
390
|
+
define_method(:setup_scenario) do
|
|
391
|
+
scenario = Scenarios[scenario_name]
|
|
392
|
+
@scenario_context = scenario.build(**options)
|
|
393
|
+
|
|
394
|
+
@scenario_context.data.each do |name, value|
|
|
395
|
+
instance_variable_set("@#{name}", value)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Hook into setup
|
|
400
|
+
prepend(Module.new do
|
|
401
|
+
def setup
|
|
402
|
+
setup_scenario
|
|
403
|
+
super
|
|
404
|
+
end
|
|
405
|
+
end)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def scenario
|
|
410
|
+
@scenario_context
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Register with RSpec if available
|
|
416
|
+
# :nocov: the `defined?(RSpec)` false arm is unreachable under the test suite (RSpec is always loaded)
|
|
417
|
+
if defined?(RSpec)
|
|
418
|
+
RSpec.configure do |config|
|
|
419
|
+
config.extend Scenarios::RSpecHelpers
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
# :nocov:
|
|
423
|
+
end
|