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,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Personas: Named Data Profiles
5
+ #
6
+ # Define reusable data profiles that represent different user types,
7
+ # test cases, or data states. More semantic than "modes" - think
8
+ # in terms of WHO you're generating data for.
9
+ #
10
+ # @example Define personas
11
+ # Synthra::Personas.define(:happy_path) do
12
+ # mode :random
13
+ # override User, email_verified: true, subscription: "premium"
14
+ # end
15
+ #
16
+ # Synthra::Personas.define(:edge_case_user) do
17
+ # mode :edge
18
+ # override User, name: "X", email: "a@b.co"
19
+ # end
20
+ #
21
+ # @example Use personas
22
+ # Synthra.as(:happy_path) { User.generate }
23
+ # Synthra.as(:edge_case_user) { User.generate }
24
+ #
25
+ module Personas
26
+ class << self
27
+ # Registry of defined personas
28
+ def registry
29
+ @registry ||= {}
30
+ end
31
+
32
+ # Define a new persona
33
+ #
34
+ # @param name [Symbol] persona name
35
+ # @yield persona configuration block
36
+ # @return [Persona] the defined persona
37
+ #
38
+ def define(name, &block)
39
+ persona = Persona.new(name)
40
+ persona.instance_eval(&block) if block
41
+ registry[name] = persona
42
+ persona
43
+ end
44
+
45
+ # Get a persona by name
46
+ #
47
+ # @param name [Symbol] persona name
48
+ # @return [Persona, nil]
49
+ #
50
+ def [](name)
51
+ registry[name]
52
+ end
53
+
54
+ # List all personas
55
+ #
56
+ # @return [Array<Symbol>]
57
+ #
58
+ def list
59
+ registry.keys
60
+ end
61
+
62
+ # Generate data using a persona
63
+ #
64
+ # @param persona_name [Symbol] persona to use
65
+ # @yield block for generation
66
+ # @return [Object] result of the block
67
+ #
68
+ def as(persona_name, &block)
69
+ persona = registry[persona_name]
70
+ raise ArgumentError, "Unknown persona: #{persona_name}" unless persona
71
+
72
+ persona.apply(&block)
73
+ end
74
+
75
+ # Load personas from configuration
76
+ #
77
+ # @param config [Hash] configuration hash
78
+ #
79
+ def load_config(config)
80
+ config.each do |name, definition|
81
+ define(name.to_sym) do
82
+ # :nocov: the `&.` nil-arm is unreachable — the trailing `if definition["mode"]` guard
83
+ # already guarantees definition["mode"] is truthy whenever this line runs.
84
+ mode definition["mode"]&.to_sym if definition["mode"]
85
+ # :nocov:
86
+ seed definition["seed"] if definition["seed"]
87
+
88
+ definition["overrides"]&.each do |schema, overrides|
89
+ override schema, **Personas.send(:symbolize_keys, overrides)
90
+ end
91
+
92
+ definition["traits"]&.each do |trait|
93
+ include_trait trait.to_sym
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ # Load personas from YAML file
100
+ #
101
+ # @param path [String] path to YAML file
102
+ #
103
+ def load_file(path)
104
+ require "yaml"
105
+ config = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
106
+ load_config(config["personas"] || config)
107
+ end
108
+
109
+ # Reset all personas (clears custom personas, preserves built-ins)
110
+ def reset!
111
+ @registry = {}
112
+ define_built_in_personas!
113
+ end
114
+
115
+ # Define built-in personas
116
+ def define_built_in_personas!
117
+ define(:default) do
118
+ mode :random
119
+ end
120
+
121
+ define(:happy_path) do
122
+ mode :random
123
+ # Most common, valid data
124
+ end
125
+
126
+ define(:edge_cases) do
127
+ mode :edge
128
+ # Boundary values, empty strings, max lengths
129
+ end
130
+
131
+ define(:invalid) do
132
+ mode :invalid
133
+ # Invalid data for validation testing
134
+ end
135
+
136
+ define(:security_test) do
137
+ mode :hostile
138
+ # SQL injection, XSS, malicious payloads
139
+ end
140
+
141
+ define(:stress_test) do
142
+ mode :edge
143
+ transform do |data, _context|
144
+ # Make strings very long
145
+ data.transform_values do |v|
146
+ v.is_a?(String) ? v * 100 : v
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def symbolize_keys(hash)
155
+ hash.transform_keys(&:to_sym)
156
+ end
157
+ end
158
+
159
+ # A persona definition
160
+ class Persona
161
+ attr_reader :name, :generation_mode, :generation_seed
162
+ attr_reader :overrides, :traits, :transforms
163
+
164
+ def initialize(name)
165
+ @name = name
166
+ @generation_mode = :random
167
+ @generation_seed = nil
168
+ @overrides = {}
169
+ @traits = []
170
+ @transforms = []
171
+ @before_hooks = []
172
+ @after_hooks = []
173
+ end
174
+
175
+ # Set generation mode
176
+ #
177
+ # @param mode_name [Symbol] :random, :edge, :invalid, :hostile, :mixed
178
+ #
179
+ def mode(mode_name)
180
+ @generation_mode = mode_name
181
+ end
182
+
183
+ # Set random seed
184
+ #
185
+ # @param value [Integer] seed value
186
+ #
187
+ def seed(value)
188
+ @generation_seed = value
189
+ end
190
+
191
+ # Add overrides for a schema
192
+ #
193
+ # @param schema [String, Symbol, Class] schema name or class
194
+ # @param fields [Hash] field overrides
195
+ #
196
+ def override(schema, **fields)
197
+ schema_name = normalize_schema_name(schema)
198
+ @overrides[schema_name] ||= {}
199
+ @overrides[schema_name].merge!(fields)
200
+ end
201
+
202
+ # Include a trait
203
+ #
204
+ # @param trait_name [Symbol] trait to include
205
+ #
206
+ def include_trait(trait_name)
207
+ @traits << trait_name
208
+ end
209
+
210
+ # Add a transform function
211
+ #
212
+ # @param schema [String, Symbol] schema name (nil for all)
213
+ # @yield [data, context] transform block
214
+ #
215
+ def transform(schema = nil, &block)
216
+ @transforms << { schema: schema&.to_s, block: block }
217
+ end
218
+
219
+ # Add before hook
220
+ def before(&block)
221
+ @before_hooks << block
222
+ end
223
+
224
+ # Add after hook
225
+ def after(&block)
226
+ @after_hooks << block
227
+ end
228
+
229
+ # Apply this persona and execute block
230
+ #
231
+ # @yield block for generation
232
+ # @return [Object] result of the block
233
+ #
234
+ def apply(&block)
235
+ context = PersonaContext.new(self)
236
+ context.apply(&block)
237
+ end
238
+
239
+ # Get effective overrides for a schema
240
+ #
241
+ # @param schema_name [String] schema name
242
+ # @return [Hash] merged overrides
243
+ #
244
+ def overrides_for(schema_name)
245
+ result = {}
246
+
247
+ # Apply trait overrides first
248
+ @traits.each do |trait_name|
249
+ trait = Traits[trait_name]
250
+ result.merge!(trait.overrides_for(schema_name)) if trait
251
+ end
252
+
253
+ # Apply persona overrides
254
+ result.merge!(@overrides[schema_name] || {})
255
+
256
+ result
257
+ end
258
+
259
+ private
260
+
261
+ def normalize_schema_name(schema)
262
+ case schema
263
+ when String then schema
264
+ when Symbol then schema.to_s
265
+ when Class then schema.name
266
+ else schema.to_s
267
+ end
268
+ end
269
+ end
270
+
271
+ # Runtime context for persona application
272
+ class PersonaContext
273
+ attr_reader :persona
274
+
275
+ def initialize(persona)
276
+ @persona = persona
277
+ @generated = {}
278
+ end
279
+
280
+ def apply(&block)
281
+ # Run before hooks
282
+ @persona.instance_variable_get(:@before_hooks).each { |h| instance_exec(&h) }
283
+
284
+ # Set thread-local context
285
+ old_context = Thread.current[:synthra_persona]
286
+ Thread.current[:synthra_persona] = self
287
+
288
+ # Yield self to the block instead of instance_eval to preserve caller's context
289
+ result = block.call(self)
290
+
291
+ # Run after hooks
292
+ @persona.instance_variable_get(:@after_hooks).each { |h| instance_exec(&h) }
293
+
294
+ result
295
+ ensure
296
+ Thread.current[:synthra_persona] = old_context
297
+ end
298
+
299
+ # Generate data with persona settings
300
+ def generate(schema_name, **options)
301
+ schema = Synthra.registry.schema(schema_name.to_s)
302
+
303
+ merged_options = {
304
+ mode: @persona.generation_mode,
305
+ seed: @persona.generation_seed,
306
+ overrides: @persona.overrides_for(schema_name.to_s)
307
+ }.merge(options)
308
+
309
+ data = schema.generate(**merged_options)
310
+
311
+ # Apply transforms
312
+ @persona.transforms.each do |transform|
313
+ if transform[:schema].nil? || transform[:schema] == schema_name.to_s
314
+ data = transform[:block].call(data, self)
315
+ end
316
+ end
317
+
318
+ @generated[schema_name.to_s] = data
319
+ data
320
+ end
321
+ end
322
+ end
323
+
324
+ # Reusable traits for personas
325
+ module Traits
326
+ class << self
327
+ def registry
328
+ @registry ||= {}
329
+ end
330
+
331
+ # Define a trait
332
+ #
333
+ # @param name [Symbol] trait name
334
+ # @yield trait configuration
335
+ #
336
+ def define(name, &block)
337
+ trait = Trait.new(name)
338
+ trait.instance_eval(&block) if block
339
+ registry[name] = trait
340
+ end
341
+
342
+ def [](name)
343
+ registry[name]
344
+ end
345
+
346
+ def reset!
347
+ @registry = {}
348
+ end
349
+ end
350
+
351
+ class Trait
352
+ attr_reader :name, :overrides
353
+
354
+ def initialize(name)
355
+ @name = name
356
+ @overrides = {}
357
+ end
358
+
359
+ def override(schema, **fields)
360
+ schema_name = schema.to_s
361
+ @overrides[schema_name] ||= {}
362
+ @overrides[schema_name].merge!(fields)
363
+ end
364
+
365
+ def overrides_for(schema_name)
366
+ @overrides[schema_name] || {}
367
+ end
368
+ end
369
+ end
370
+
371
+ # Extend main module with persona support
372
+ class << self
373
+ # Generate data using a persona
374
+ #
375
+ # @param persona_name [Symbol] persona to use
376
+ # @yield block for generation
377
+ # @return [Object] result
378
+ #
379
+ # @example
380
+ # Synthra.as(:happy_path) { generate("User") }
381
+ #
382
+ def as(persona_name, &block)
383
+ Personas.as(persona_name, &block)
384
+ end
385
+
386
+ # Get current persona context
387
+ #
388
+ # @return [Personas::PersonaContext, nil]
389
+ #
390
+ def current_persona
391
+ Thread.current[:synthra_persona]
392
+ end
393
+ end
394
+
395
+ # Initialize built-in personas
396
+ Personas.define_built_in_personas!
397
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+ # Synthra Property-Based Testing Integration
5
+ # =============================================================================
6
+ #
7
+ # Provides RSpec helpers for property-based testing with Synthra schemas.
8
+ # Allows you to verify properties hold for all generated data.
9
+ #
10
+ # @example Basic property test
11
+ # RSpec.describe "User Registration" do
12
+ # include Synthra::PropertyTesting
13
+ #
14
+ # it "accepts all valid users" do
15
+ # verify_property("User") do |user_data|
16
+ # user = User.create(user_data)
17
+ # expect(user).to be_valid
18
+ # expect(user.save).to be_truthy
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # @example With custom count and seed
24
+ # verify_property("Order", count: 1000, seed: 42) do |order_data|
25
+ # expect(order_data["total"]).to be >= 0
26
+ # end
27
+ #
28
+ # =============================================================================
29
+
30
+ module Synthra
31
+ module PropertyTesting
32
+ # Verify a property holds for generated data
33
+ #
34
+ # Generates multiple records from a schema and verifies a property
35
+ # holds for all of them. Raises if property fails for any record.
36
+ #
37
+ # @param schema_name [String] schema name to generate from
38
+ # @param count [Integer] number of records to generate (default: 100)
39
+ # @param seed [Integer, nil] random seed for reproducibility
40
+ # @param mode [Symbol] generation mode (default: :random)
41
+ # @param registry [Registry, nil] registry for schema lookup
42
+ # @yield [Hash] block to verify property
43
+ # @yieldparam record [Hash] generated record
44
+ # @raise [RSpec::Expectations::ExpectationNotMetError] if property fails
45
+ #
46
+ # @example
47
+ # verify_property("User") do |user|
48
+ # expect(user["email"]).to match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
49
+ # end
50
+ #
51
+ def verify_property(schema_name, count: 100, seed: nil, mode: :random, registry: nil)
52
+ registry ||= Registry.new
53
+ schema = registry.schema(schema_name)
54
+
55
+ records = schema.generate_many(count, seed: seed, mode: mode, registry: registry)
56
+
57
+ records.each_with_index do |record, idx|
58
+ begin
59
+ yield(record)
60
+ rescue RSpec::Expectations::ExpectationNotMetError => e
61
+ raise RSpec::Expectations::ExpectationNotMetError,
62
+ "Property failed for record #{idx + 1}/#{count} (seed: #{seed || 'random'}):\n" \
63
+ "#{e.message}\n" \
64
+ "Record: #{record.inspect}"
65
+ end
66
+ end
67
+ end
68
+
69
+ # Verify a property holds for a stream of generated data
70
+ #
71
+ # Generates records lazily and verifies property for each.
72
+ # More memory-efficient for large counts.
73
+ #
74
+ # @param schema_name [String] schema name to generate from
75
+ # @param count [Integer] number of records to generate
76
+ # @param seed [Integer, nil] random seed
77
+ # @param mode [Symbol] generation mode
78
+ # @param registry [Registry, nil] registry for schema lookup
79
+ # @yield [Hash] block to verify property
80
+ #
81
+ # @example
82
+ # verify_property_stream("Order", count: 10000) do |order|
83
+ # expect(order["items"]).to be_an(Array)
84
+ # end
85
+ #
86
+ def verify_property_stream(schema_name, count: 1000, seed: nil, mode: :random, registry: nil)
87
+ registry ||= Registry.new
88
+ schema = registry.schema(schema_name)
89
+
90
+ stream = schema.generate_many_stream(count, seed: seed, mode: mode, registry: registry)
91
+
92
+ stream.each_with_index do |record, idx|
93
+ begin
94
+ yield(record)
95
+ rescue RSpec::Expectations::ExpectationNotMetError => e
96
+ raise RSpec::Expectations::ExpectationNotMetError,
97
+ "Property failed for record #{idx + 1}/#{count} (seed: #{seed || 'random'}):\n" \
98
+ "#{e.message}\n" \
99
+ "Record: #{record.inspect}"
100
+ end
101
+ end
102
+ end
103
+
104
+ # Shrink a failing test case to find minimal counterexample
105
+ #
106
+ # When a property fails, attempts to shrink the failing record
107
+ # to find a smaller counterexample.
108
+ #
109
+ # @param schema_name [String] schema name
110
+ # @param failing_record [Hash] record that caused failure
111
+ # @param registry [Registry, nil] registry
112
+ # @yield [Hash] block to verify property
113
+ # @return [Hash] minimal counterexample
114
+ #
115
+ def shrink_counterexample(schema_name, failing_record, registry: nil, &block)
116
+ registry ||= Registry.new
117
+ schema = registry.schema(schema_name)
118
+
119
+ current = failing_record.dup
120
+ improved = true
121
+
122
+ while improved
123
+ improved = false
124
+
125
+ # Try removing optional fields
126
+ current.keys.each do |key|
127
+ next unless schema.field(key)&.optional?
128
+
129
+ candidate = current.dup
130
+ candidate.delete(key)
131
+
132
+ begin
133
+ block.call(candidate)
134
+ rescue RSpec::Expectations::ExpectationNotMetError
135
+ # Still fails with smaller record
136
+ current = candidate
137
+ improved = true
138
+ break
139
+ end
140
+ end
141
+
142
+ # Try simplifying array values
143
+ current.each do |key, value|
144
+ next unless value.is_a?(Array) && value.length > 1
145
+
146
+ candidate = current.dup
147
+ candidate[key] = value[0..0] # Single element
148
+
149
+ begin
150
+ block.call(candidate)
151
+ rescue RSpec::Expectations::ExpectationNotMetError
152
+ current = candidate
153
+ improved = true
154
+ break
155
+ end
156
+ end
157
+ end
158
+
159
+ current
160
+ end
161
+ end
162
+ end
163
+
164
+ # Include in RSpec
165
+ # :nocov: — load-time RSpec wiring. This file is only required while RSpec is
166
+ # loaded, so `defined?(RSpec)` is always truthy here; the false arm is unreachable.
167
+ if defined?(RSpec)
168
+ RSpec.configure do |config|
169
+ config.include Synthra::PropertyTesting
170
+ end
171
+ end
172
+ # :nocov: