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.
Files changed (302) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +40 -0
  4. data/.yardopts +18 -0
  5. data/ACCESS_DOCS.md +116 -0
  6. data/ADVANCED_FEATURES_SUMMARY.md +245 -0
  7. data/CHANGELOG.md +498 -0
  8. data/CODE_OF_CONDUCT.md +122 -0
  9. data/CONTRIBUTING.md +307 -0
  10. data/DOCUMENTATION.md +204 -0
  11. data/DOCUMENTATION_GUIDE.md +194 -0
  12. data/LICENSE +22 -0
  13. data/README.md +1458 -0
  14. data/Rakefile +128 -0
  15. data/benchmark/README.md +591 -0
  16. data/benchmark/batch_generation.rb +63 -0
  17. data/benchmark/engine_comparison.rb +295 -0
  18. data/benchmark/single_record.rb +108 -0
  19. data/benchmark/streaming.rb +74 -0
  20. data/docs/ACTIVERECORD_INFERENCE.md +524 -0
  21. data/docs/API_SERVER.md +278 -0
  22. data/docs/CONFIG_FILE.md +315 -0
  23. data/docs/DATA_CONTRACTS.md +312 -0
  24. data/docs/ENHANCED_REPL.md +304 -0
  25. data/docs/FACTORY_BOT.md +271 -0
  26. data/docs/GITHUB_ACTION.md +420 -0
  27. data/docs/GRAPHQL_EXPORT.md +303 -0
  28. data/docs/GRAPHQL_FEDERATION.md +651 -0
  29. data/docs/HOW_TO_GENERATE_DOCS.md +252 -0
  30. data/docs/LSP.md +189 -0
  31. data/docs/MIGRATION_GENERATOR.md +547 -0
  32. data/docs/MOCK_SERVER.md +280 -0
  33. data/docs/NATIVE_ENGINE.md +298 -0
  34. data/docs/OPENAPI_EXPORT.md +252 -0
  35. data/docs/PERFORMANCE_MODE.md +228 -0
  36. data/docs/PERSONAS.md +547 -0
  37. data/docs/PROPERTY_TESTING.md +284 -0
  38. data/docs/PROTOBUF_EXPORT.md +276 -0
  39. data/docs/QUALITY_METRICS.md +555 -0
  40. data/docs/QUICK_REFERENCE.md +88 -0
  41. data/docs/RAILS_ENGINE.md +404 -0
  42. data/docs/RAILS_INTEGRATION.md +454 -0
  43. data/docs/README.md +175 -0
  44. data/docs/REPL_QUICK_REFERENCE.md +149 -0
  45. data/docs/RUST_INTEGRATION_GUIDE.md +1376 -0
  46. data/docs/SCENARIOS.md +576 -0
  47. data/docs/SECURITY_TESTING.md +318 -0
  48. data/docs/SNAPSHOT_TESTING.md +308 -0
  49. data/docs/STRING_TEXT_TYPES_REFERENCE.md +512 -0
  50. data/docs/TERRAFORM_EXPORT.md +447 -0
  51. data/docs/TIME_TRAVEL.md +507 -0
  52. data/docs/USAGE_GUIDE.md +980 -0
  53. data/docs/WEBHOOK_SIMULATOR.md +579 -0
  54. data/docs/advanced_features.md +183 -0
  55. data/docs/api_reference.md +153 -0
  56. data/docs/behaviors.md +138 -0
  57. data/docs/best_practices.md +191 -0
  58. data/docs/dsl_reference.md +172 -0
  59. data/docs/examples.md +162 -0
  60. data/docs/generation_modes.md +125 -0
  61. data/docs/getting_started.md +92 -0
  62. data/docs/index.html +177 -0
  63. data/docs/strategic/CLOUD_REGISTRY_ARCHITECTURE.md +268 -0
  64. data/docs/strategic/SIMD_OPTIMIZATION_STRATEGY.md +153 -0
  65. data/docs/troubleshooting.md +215 -0
  66. data/docs/type_reference.md +224 -0
  67. data/examples/hostile_mode_demo.rb +64 -0
  68. data/exe/synthra +6 -0
  69. data/ext/synthra_native/Cargo.lock +972 -0
  70. data/ext/synthra_native/Cargo.toml +46 -0
  71. data/ext/synthra_native/extconf.rb +41 -0
  72. data/ext/synthra_native/src/generator.rs +397 -0
  73. data/ext/synthra_native/src/lib.rs +229 -0
  74. data/ext/synthra_native/src/types.rs +1485 -0
  75. data/lib/generators/synthra/install_generator.rb +86 -0
  76. data/lib/generators/synthra/templates/api_response.dsl +15 -0
  77. data/lib/generators/synthra/templates/fake_data.rake +116 -0
  78. data/lib/generators/synthra/templates/synthra.yml +31 -0
  79. data/lib/generators/synthra/templates/synthra_support.rb +19 -0
  80. data/lib/generators/synthra/templates/user.dsl +11 -0
  81. data/lib/synthra/activerecord_inference.rb +394 -0
  82. data/lib/synthra/api.rb +381 -0
  83. data/lib/synthra/api_server.rb +718 -0
  84. data/lib/synthra/behaviors/applicator.rb +165 -0
  85. data/lib/synthra/behaviors/base.rb +155 -0
  86. data/lib/synthra/behaviors/close_connection.rb +60 -0
  87. data/lib/synthra/behaviors/deprecated.rb +100 -0
  88. data/lib/synthra/behaviors/failure.rb +69 -0
  89. data/lib/synthra/behaviors/latency.rb +103 -0
  90. data/lib/synthra/behaviors/partial_data.rb +83 -0
  91. data/lib/synthra/behaviors/randomize_order.rb +74 -0
  92. data/lib/synthra/behaviors/registry.rb +238 -0
  93. data/lib/synthra/behaviors/simulate_error.rb +79 -0
  94. data/lib/synthra/cli/commands/base.rb +105 -0
  95. data/lib/synthra/cli/commands/diff.rb +227 -0
  96. data/lib/synthra/cli/commands/docs.rb +67 -0
  97. data/lib/synthra/cli/commands/export.rb +150 -0
  98. data/lib/synthra/cli/commands/generate.rb +72 -0
  99. data/lib/synthra/cli/commands/import.rb +80 -0
  100. data/lib/synthra/cli/commands/lint.rb +53 -0
  101. data/lib/synthra/cli/commands/live.rb +57 -0
  102. data/lib/synthra/cli/commands/seed.rb +99 -0
  103. data/lib/synthra/cli/commands/validate.rb +28 -0
  104. data/lib/synthra/cli.rb +2471 -0
  105. data/lib/synthra/config_file.rb +129 -0
  106. data/lib/synthra/configuration.rb +281 -0
  107. data/lib/synthra/contracts_registry.rb +408 -0
  108. data/lib/synthra/database_seeder.rb +268 -0
  109. data/lib/synthra/deterministic_ids.rb +218 -0
  110. data/lib/synthra/documentation_generator.rb +414 -0
  111. data/lib/synthra/engine.rb +251 -0
  112. data/lib/synthra/errors.rb +1169 -0
  113. data/lib/synthra/export/base.rb +85 -0
  114. data/lib/synthra/export/csv.rb +101 -0
  115. data/lib/synthra/export/graphql.rb +266 -0
  116. data/lib/synthra/export/graphql_federation.rb +377 -0
  117. data/lib/synthra/export/graphviz.rb +258 -0
  118. data/lib/synthra/export/javascript.rb +327 -0
  119. data/lib/synthra/export/json_data.rb +61 -0
  120. data/lib/synthra/export/json_schema.rb +290 -0
  121. data/lib/synthra/export/openapi.rb +514 -0
  122. data/lib/synthra/export/protobuf.rb +483 -0
  123. data/lib/synthra/export/python.rb +560 -0
  124. data/lib/synthra/export/sql.rb +381 -0
  125. data/lib/synthra/export/sql_insert.rb +152 -0
  126. data/lib/synthra/export/terraform.rb +501 -0
  127. data/lib/synthra/export/type_mapping.rb +389 -0
  128. data/lib/synthra/export/typescript.rb +300 -0
  129. data/lib/synthra/export/xml_data.rb +104 -0
  130. data/lib/synthra/export/yaml_data.rb +56 -0
  131. data/lib/synthra/export.rb +404 -0
  132. data/lib/synthra/factory_bot_integration.rb +157 -0
  133. data/lib/synthra/field.rb +440 -0
  134. data/lib/synthra/functions/registry.rb +104 -0
  135. data/lib/synthra/generator/context.rb +372 -0
  136. data/lib/synthra/generator/engine.rb +336 -0
  137. data/lib/synthra/generator/faker_adapter.rb +425 -0
  138. data/lib/synthra/generator/modes.rb +444 -0
  139. data/lib/synthra/generator/resolver.rb +256 -0
  140. data/lib/synthra/generator/rng.rb +273 -0
  141. data/lib/synthra/generator/streamer.rb +63 -0
  142. data/lib/synthra/generator/uniqueness.rb +118 -0
  143. data/lib/synthra/initializer.rb +97 -0
  144. data/lib/synthra/limits.rb +279 -0
  145. data/lib/synthra/live_preview.rb +518 -0
  146. data/lib/synthra/loader_config.rb +78 -0
  147. data/lib/synthra/lsp/server.rb +689 -0
  148. data/lib/synthra/migration_generator.rb +301 -0
  149. data/lib/synthra/mixin.rb +174 -0
  150. data/lib/synthra/mock_server.rb +458 -0
  151. data/lib/synthra/native_engine.rb +304 -0
  152. data/lib/synthra/openapi_importer.rb +228 -0
  153. data/lib/synthra/output/json_formatter.rb +40 -0
  154. data/lib/synthra/output/ndjson_formatter.rb +55 -0
  155. data/lib/synthra/parser/ast.rb +1287 -0
  156. data/lib/synthra/parser/lexer.rb +1152 -0
  157. data/lib/synthra/parser/parser.rb +1664 -0
  158. data/lib/synthra/parser/tokens.rb +461 -0
  159. data/lib/synthra/performance_mode.rb +364 -0
  160. data/lib/synthra/personas.rb +397 -0
  161. data/lib/synthra/property_testing.rb +172 -0
  162. data/lib/synthra/quality_metrics.rb +405 -0
  163. data/lib/synthra/rails_test_helper.rb +248 -0
  164. data/lib/synthra/registry.rb +533 -0
  165. data/lib/synthra/relationships.rb +193 -0
  166. data/lib/synthra/repl/enhanced_repl.rb +607 -0
  167. data/lib/synthra/repl/formatter.rb +191 -0
  168. data/lib/synthra/scenarios.rb +423 -0
  169. data/lib/synthra/schema.rb +605 -0
  170. data/lib/synthra/schema_inheritance.rb +104 -0
  171. data/lib/synthra/schema_versioning.rb +212 -0
  172. data/lib/synthra/snapshot_testing.rb +199 -0
  173. data/lib/synthra/time_travel.rb +338 -0
  174. data/lib/synthra/type_definitions.rb +274 -0
  175. data/lib/synthra/types/address_location/addresses.rb +125 -0
  176. data/lib/synthra/types/address_location/airports.rb +200 -0
  177. data/lib/synthra/types/address_location/banks_hospitals.rb +207 -0
  178. data/lib/synthra/types/address_location/locations.rb +406 -0
  179. data/lib/synthra/types/base.rb +48 -0
  180. data/lib/synthra/types/commerce_products/commerce.rb +119 -0
  181. data/lib/synthra/types/commerce_products/companies.rb +71 -0
  182. data/lib/synthra/types/commerce_products/construction.rb +126 -0
  183. data/lib/synthra/types/commerce_products/products.rb +205 -0
  184. data/lib/synthra/types/core/collections.rb +513 -0
  185. data/lib/synthra/types/core/defaults.rb +76 -0
  186. data/lib/synthra/types/core/enums.rb +102 -0
  187. data/lib/synthra/types/core/identifiers.rb +445 -0
  188. data/lib/synthra/types/core/primitives.rb +586 -0
  189. data/lib/synthra/types/core/references.rb +466 -0
  190. data/lib/synthra/types/core/sequences.rb +304 -0
  191. data/lib/synthra/types/crypto/crypto.rb +162 -0
  192. data/lib/synthra/types/date_time/dates.rb +387 -0
  193. data/lib/synthra/types/finance_banking/banking.rb +424 -0
  194. data/lib/synthra/types/finance_banking/credit_cards.rb +24 -0
  195. data/lib/synthra/types/finance_banking/identifiers.rb +93 -0
  196. data/lib/synthra/types/formula.rb +184 -0
  197. data/lib/synthra/types/health_medical/medical.rb +150 -0
  198. data/lib/synthra/types/hostile_payloads.rb +122 -0
  199. data/lib/synthra/types/json_array.rb +51 -0
  200. data/lib/synthra/types/media_entertainment/media.rb +62 -0
  201. data/lib/synthra/types/naughty_string.rb +30 -0
  202. data/lib/synthra/types/personal_names/chinese.rb +41 -0
  203. data/lib/synthra/types/personal_names/identifiers.rb +187 -0
  204. data/lib/synthra/types/personal_names/names.rb +246 -0
  205. data/lib/synthra/types/personal_names/national_id.rb +89 -0
  206. data/lib/synthra/types/personal_names/titles_suffixes.rb +41 -0
  207. data/lib/synthra/types/regex.rb +248 -0
  208. data/lib/synthra/types/registry.rb +182 -0
  209. data/lib/synthra/types/repeating_element.rb +50 -0
  210. data/lib/synthra/types/scenario.rb +29 -0
  211. data/lib/synthra/types/technology_internet/apps.rb +67 -0
  212. data/lib/synthra/types/technology_internet/communication.rb +162 -0
  213. data/lib/synthra/types/technology_internet/devices.rb +80 -0
  214. data/lib/synthra/types/technology_internet/formats.rb +139 -0
  215. data/lib/synthra/types/technology_internet/networking.rb +143 -0
  216. data/lib/synthra/types/template.rb +92 -0
  217. data/lib/synthra/types/text_content/business.rb +128 -0
  218. data/lib/synthra/types/text_content/colors.rb +70 -0
  219. data/lib/synthra/types/text_content/misc.rb +237 -0
  220. data/lib/synthra/types/text_content/security.rb +130 -0
  221. data/lib/synthra/types/text_content/text_generation.rb +532 -0
  222. data/lib/synthra/types/travel/travel.rb +150 -0
  223. data/lib/synthra/utils/string_distance.rb +133 -0
  224. data/lib/synthra/validator/dsl_validator.rb +339 -0
  225. data/lib/synthra/validator/path_validator.rb +615 -0
  226. data/lib/synthra/version.rb +35 -0
  227. data/lib/synthra/webhook_simulator.rb +341 -0
  228. data/lib/synthra.rb +259 -0
  229. data/schemas/address.dsl +16 -0
  230. data/schemas/api_response.dsl +8 -0
  231. data/schemas/error_payload.dsl +9 -0
  232. data/schemas/order.dsl +10 -0
  233. data/schemas/order_item.dsl +8 -0
  234. data/schemas/payment.dsl +11 -0
  235. data/schemas/social_post.dsl +13 -0
  236. data/schemas/user.dsl +10 -0
  237. data/scripts/batch_fix_all.rb +96 -0
  238. data/scripts/delete_old_files.rb +32 -0
  239. data/scripts/fix_all_grouped_files.rb +137 -0
  240. data/scripts/fix_all_indentation.rb +48 -0
  241. data/scripts/fix_all_syntax.rb +124 -0
  242. data/scripts/fix_grouped_files.rb +184 -0
  243. data/scripts/fix_syntax_errors.rb +108 -0
  244. data/scripts/group_domain_types.rb +112 -0
  245. data/scripts/group_domain_types_fixed.rb +150 -0
  246. data/scripts/merge_domains_to_one_file.rb +68 -0
  247. data/scripts/move_existing_types.rb +130 -0
  248. data/scripts/split_grouped_types.rb +142 -0
  249. data/tech_docs/README.md +134 -0
  250. data/tech_docs/advanced/streaming.md +405 -0
  251. data/tech_docs/advanced/thread_safety.md +253 -0
  252. data/tech_docs/api/overview.md +485 -0
  253. data/tech_docs/appendices/type_chart.md +193 -0
  254. data/tech_docs/basic_concepts.md +383 -0
  255. data/tech_docs/behaviors/overview.md +346 -0
  256. data/tech_docs/dsl/complex_types.md +790 -0
  257. data/tech_docs/dsl/core_types.md +464 -0
  258. data/tech_docs/dsl/datetime_types.md +325 -0
  259. data/tech_docs/dsl/field_modifiers.md +414 -0
  260. data/tech_docs/dsl/grammar.md +431 -0
  261. data/tech_docs/dsl/schema_definition.md +399 -0
  262. data/tech_docs/export/README.md +276 -0
  263. data/tech_docs/installation.md +273 -0
  264. data/tech_docs/integration/ci_cd.md +707 -0
  265. data/tech_docs/integration/ci_cd_guide.md +579 -0
  266. data/tech_docs/integration/factory_bot.md +485 -0
  267. data/tech_docs/integration/rails.md +630 -0
  268. data/tech_docs/integration/rspec.md +449 -0
  269. data/tech_docs/modes/overview.md +350 -0
  270. data/tech_docs/performance/NATIVE_RUST_EXTENSION.md +1270 -0
  271. data/tech_docs/performance/OPTIMIZATION_GUIDE.md +901 -0
  272. data/tech_docs/quick_start.md +256 -0
  273. data/tech_docs/templates/README.md +805 -0
  274. data/tech_docs/tutorials/advanced.md +371 -0
  275. data/tech_docs/tutorials/getting_started.md +189 -0
  276. data/tech_docs/tutorials/intermediate.md +231 -0
  277. data/tech_docs/tutorials/template_gallery.md +569 -0
  278. data/tech_example/01_basic_usage.rb +238 -0
  279. data/tech_example/02_types_demo.rb +336 -0
  280. data/tech_example/04_cli_usage.md +429 -0
  281. data/tech_example/05_database_seeding.rb +283 -0
  282. data/tech_example/07_rspec_integration.rb +359 -0
  283. data/tech_example/10_custom_types.rb +387 -0
  284. data/tech_example/12_behaviors.rb +372 -0
  285. data/tech_example/13_twitter_dm_example.rb +120 -0
  286. data/tech_example/14_exact_json_structure.rb +253 -0
  287. data/tech_example/16_lsp_server.rb +56 -0
  288. data/tech_example/17_property_testing.rb +92 -0
  289. data/tech_example/18_enhanced_repl.rb +120 -0
  290. data/tech_example/19_security_fuzzing.rb +145 -0
  291. data/tech_example/NEW_FEATURES.md +169 -0
  292. data/tech_example/README.md +118 -0
  293. data/tech_example/schemas/api_response.dsl +123 -0
  294. data/tech_example/schemas/ecommerce.dsl +121 -0
  295. data/tech_example/schemas/twitter_dm.dsl +109 -0
  296. data/tech_example/schemas/user.dsl +36 -0
  297. data/vscode-extension/README.md +246 -0
  298. data/vscode-extension/language-configuration.json +31 -0
  299. data/vscode-extension/package.json +55 -0
  300. data/vscode-extension/snippets/fakedatadsl.json +198 -0
  301. data/vscode-extension/syntaxes/fakedatadsl.tmLanguage.json +128 -0
  302. 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