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,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Generator Context
6
+ # =============================================================================
7
+ #
8
+ # The Context holds generated field values during record generation.
9
+ # It provides access to previously generated values for computed fields
10
+ # and conditional logic.
11
+ #
12
+ # @example Using context in custom functions
13
+ # Synthra.register_function(:full_name) do |ctx|
14
+ # "#{ctx['first_name']} #{ctx['last_name']}"
15
+ # end
16
+ #
17
+ # @example Conditional field using context
18
+ # # admin_note: text if is_admin
19
+ # # The context contains { "is_admin" => true } when evaluating
20
+ #
21
+ # =============================================================================
22
+
23
+
24
+ module Synthra
25
+ module Generator
26
+
27
+ # Generation context for accessing generated field values
28
+ #
29
+ # Context acts as a hash-like container for field values during
30
+ # generation. It supports multiple access patterns (hash, dot notation)
31
+ # and nested path lookups.
32
+ #
33
+ # @example Create and use context
34
+ # ctx = Context.new
35
+ # ctx["name"] = "John"
36
+ # ctx["name"] # => "John"
37
+ # ctx.name # => "John" (dot notation)
38
+ #
39
+ # @example Nested access
40
+ # ctx["address"] = { "city" => "NYC" }
41
+ # ctx.get("address.city") # => "NYC"
42
+ #
43
+
44
+ class Context
45
+
46
+ # @!attribute [r] schema_name
47
+ # Name of the schema being generated (for reference resolution)
48
+ # @return [String, nil] the schema name
49
+
50
+ attr_reader :schema_name
51
+
52
+
53
+ # @!attribute [rw] registry
54
+ # Registry for resolving cross-schema references
55
+ # @return [Registry, nil] the schema registry
56
+
57
+ attr_accessor :registry
58
+
59
+
60
+ # @!attribute [rw] depth
61
+ # Current recursion depth for nested schema generation
62
+ # @return [Integer] the depth level
63
+
64
+ attr_accessor :depth
65
+
66
+
67
+ # @!attribute [rw] parent_context
68
+ # Parent context for nested schemas (enables copy() from parent)
69
+ # @return [Context, nil] the parent context
70
+
71
+ attr_accessor :parent_context
72
+
73
+
74
+ # @!attribute [rw] shared_context
75
+ # Shared context for batch generation (values persist across records)
76
+ # @return [Hash] the shared context hash
77
+
78
+ attr_accessor :shared_context
79
+
80
+
81
+ # @!attribute [rw] resolver
82
+ # Resolver for cross-schema reference resolution
83
+ # @return [Resolver, nil] the resolver instance
84
+
85
+ attr_accessor :resolver
86
+
87
+
88
+ # @!attribute [rw] faker_adapter
89
+ # Faker adapter for deterministic fake data generation
90
+ # @return [FakerAdapter, nil] the faker adapter
91
+
92
+ attr_accessor :faker_adapter
93
+
94
+
95
+ # Create a new Context
96
+ #
97
+ # @param data [Hash] initial data to populate the context
98
+ # @param registry [Registry, nil] registry for resolving references
99
+ # @param depth [Integer] current recursion depth
100
+ # @param parent_context [Context, nil] parent context for nested schemas
101
+ # @param shared_context [Hash, nil] shared context for batch generation
102
+ # @param resolver [Resolver, nil] resolver for cross-schema references
103
+ # @param faker_adapter [FakerAdapter, nil] adapter for Faker
104
+ #
105
+ # @example Empty context
106
+ # ctx = Context.new
107
+ #
108
+ # @example Pre-populated context
109
+ # ctx = Context.new({ name: "John", age: 30 })
110
+ #
111
+ # @example With all options
112
+ # ctx = Context.new(
113
+ # { name: "John" },
114
+ # registry: my_registry,
115
+ # depth: 1,
116
+ # parent_context: parent_ctx,
117
+ # shared_context: {}
118
+ # )
119
+ #
120
+
121
+ def initialize(data = {}, registry: nil, depth: 0, parent_context: nil, shared_context: nil, resolver: nil, faker_adapter: nil)
122
+ @data = data.transform_keys(&:to_s)
123
+ @schema_name = nil
124
+ @registry = registry
125
+ @depth = depth
126
+ @parent_context = parent_context
127
+ @shared_context = shared_context || {}
128
+ @resolver = resolver
129
+ @faker_adapter = faker_adapter
130
+ end
131
+
132
+
133
+ # Get value by key (hash-style access)
134
+ #
135
+ # @param key [String, Symbol] the key to look up
136
+ # @return [Object, nil] the value or nil if not found
137
+ #
138
+ # @example
139
+ # ctx["name"] # => "John"
140
+ # ctx[:name] # => "John" (symbols work too)
141
+ #
142
+
143
+ def [](key)
144
+ @data[key.to_s]
145
+ end
146
+
147
+
148
+ # Set value by key (hash-style access)
149
+ #
150
+ # @param key [String, Symbol] the key to set
151
+ # @param value [Object] the value to store
152
+ # @return [Object] the value
153
+ #
154
+ # @example
155
+ # ctx["name"] = "John"
156
+ #
157
+
158
+ def []=(key, value)
159
+ @data[key.to_s] = value
160
+ end
161
+
162
+
163
+ # Set value (chainable)
164
+ #
165
+ # @param key [String, Symbol] the key to set
166
+ # @param value [Object] the value to store
167
+ # @return [Context] self for chaining
168
+ #
169
+ # @example
170
+ # ctx.set(:name, "John").set(:age, 30)
171
+ #
172
+
173
+ def set(key, value)
174
+ self[key] = value
175
+ self
176
+ end
177
+
178
+
179
+ # Get value with dot notation path support
180
+ #
181
+ # Traverses nested hashes/contexts using dot-separated paths.
182
+ #
183
+ # @param path [String] dot-separated path (e.g., "address.city")
184
+ # @return [Object, nil] the value or nil if not found
185
+ #
186
+ # @example
187
+ # ctx["user"] = { "address" => { "city" => "NYC" } }
188
+ # ctx.get("user.address.city") # => "NYC"
189
+ #
190
+
191
+ def get(path)
192
+ parts = path.to_s.split(".")
193
+ result = @data
194
+
195
+ parts.each do |part|
196
+ case result
197
+ when Hash
198
+ result = result[part] || result[part.to_sym]
199
+ when Context
200
+ result = result[part]
201
+ else
202
+ return nil
203
+ end
204
+ return nil if result.nil?
205
+ end
206
+
207
+ result
208
+ end
209
+
210
+
211
+ # Check if a key exists
212
+ #
213
+ # @param key [String, Symbol] the key to check
214
+ # @return [Boolean] true if key exists
215
+ #
216
+ # @example
217
+ # ctx.has?("name") # => true
218
+ # ctx.has?("unknown") # => false
219
+ #
220
+
221
+ def has?(key)
222
+ @data.key?(key.to_s)
223
+ end
224
+
225
+ alias key? has?
226
+ alias include? has?
227
+
228
+
229
+ # Convert to a plain Hash
230
+ #
231
+ # @return [Hash] copy of internal data
232
+ #
233
+ # @example
234
+ # ctx.to_h # => { "name" => "John", "age" => 30 }
235
+ #
236
+
237
+ def to_h
238
+ @data.dup
239
+ end
240
+
241
+ alias to_hash to_h
242
+
243
+
244
+ # Merge with another hash or context
245
+ #
246
+ # Creates a new Context with merged data. Does not modify original.
247
+ #
248
+ # @param other [Hash, Context] values to merge in
249
+ # @return [Context] new context with merged data
250
+ #
251
+ # @example
252
+ # new_ctx = ctx.merge({ email: "john@example.com" })
253
+ #
254
+
255
+ def merge(other)
256
+ other_data = other.respond_to?(:to_h) ? other.to_h : other
257
+ Context.new(@data.merge(other_data.transform_keys(&:to_s)))
258
+ end
259
+
260
+
261
+ # Set the schema name for reference resolution
262
+ #
263
+ # @param name [String] the schema name
264
+ # @return [Context] self for chaining
265
+ #
266
+
267
+ def with_schema(name)
268
+ @schema_name = name
269
+ self
270
+ end
271
+
272
+
273
+ # Dynamic method access (dot notation for fields)
274
+ #
275
+ # Provides Ruby-style attribute access to context values.
276
+ # Nested hashes are automatically wrapped in Context objects.
277
+ #
278
+ # @example
279
+ # ctx.name # same as ctx["name"]
280
+ # ctx.name = "X" # same as ctx["name"] = "X"
281
+ #
282
+
283
+ def method_missing(method, *args, &block)
284
+ method_name = method.to_s
285
+
286
+
287
+ # Handle setters (method=)
288
+ if method_name.end_with?("=")
289
+ key = method_name.chomp("=")
290
+ return self[key] = args.first
291
+ end
292
+
293
+
294
+ # Handle getters
295
+ if @data.key?(method_name)
296
+ value = @data[method_name]
297
+
298
+ # Wrap nested hashes in Context for continued dot access
299
+ return value.is_a?(Hash) ? Context.new(value) : value
300
+ end
301
+
302
+ super
303
+ end
304
+
305
+
306
+ # Check if method responds (for method_missing)
307
+ #
308
+ # @param method [Symbol] method name
309
+ # @param include_private [Boolean] include private methods
310
+ # @return [Boolean] true if method would be handled
311
+ #
312
+
313
+ def respond_to_missing?(method, include_private = false)
314
+ method_name = method.to_s.chomp("=")
315
+ @data.key?(method_name) || super
316
+ end
317
+
318
+
319
+ # Get all keys
320
+ #
321
+ # @return [Array<String>] list of keys
322
+ #
323
+
324
+ def keys
325
+ @data.keys
326
+ end
327
+
328
+
329
+ # Get all values
330
+ #
331
+ # @return [Array] list of values
332
+ #
333
+
334
+ def values
335
+ @data.values
336
+ end
337
+
338
+
339
+ # Iterate over key-value pairs
340
+ #
341
+ # @yield [key, value] block for each pair
342
+ # @return [Enumerator] if no block given
343
+ #
344
+
345
+ def each(&block)
346
+ @data.each(&block)
347
+ end
348
+
349
+
350
+ # Check if context is empty
351
+ #
352
+ # @return [Boolean] true if no data
353
+ #
354
+
355
+ def empty?
356
+ @data.empty?
357
+ end
358
+
359
+
360
+ # Get number of entries
361
+ #
362
+ # @return [Integer] number of key-value pairs
363
+ #
364
+
365
+ def size
366
+ @data.size
367
+ end
368
+
369
+ alias length size
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Generator Engine
6
+ # =============================================================================
7
+ #
8
+ # The Engine is the core component responsible for generating fake data
9
+ # from schema definitions. It coordinates type generation, field behaviors,
10
+ # uniqueness constraints, and deterministic seeding.
11
+ #
12
+ # @example Basic usage
13
+ # engine = Generator::Engine.new(schema)
14
+ # record = engine.generate
15
+ #
16
+ # @example Deterministic generation
17
+ # engine = Generator::Engine.new(schema)
18
+ # record1 = engine.generate(seed: 12345)
19
+ # record2 = engine.generate(seed: 12345)
20
+ # # record1 == record2 (same seed = same output)
21
+ #
22
+ # =============================================================================
23
+
24
+
25
+ module Synthra
26
+ module Generator
27
+
28
+ # Core generation engine
29
+ #
30
+ # The Engine orchestrates the data generation process, handling:
31
+ # - Field iteration and value generation
32
+ # - Type lookup and invocation
33
+ # - Uniqueness constraint enforcement
34
+ # - Optional and conditional field handling
35
+ # - Field-level behavior application
36
+ # - Deterministic seeding via RNG
37
+ #
38
+ # @example Generate a single record
39
+ # engine = Engine.new(schema)
40
+ # user = engine.generate(seed: 42, mode: :random)
41
+ #
42
+ # @example Generate multiple records
43
+ # users = engine.generate_many(100, seed: 42)
44
+ #
45
+
46
+ class Engine
47
+
48
+ # @!attribute [r] schema
49
+ # The schema being generated from
50
+ # @return [Schema] the schema definition
51
+
52
+ attr_reader :schema
53
+
54
+
55
+ # @!attribute [r] rng
56
+ # The random number generator for determinism
57
+ # @return [RNG, nil] the RNG instance
58
+
59
+ attr_reader :rng
60
+
61
+
62
+ # @!attribute [r] registry
63
+ # Registry for cross-schema references
64
+ # @return [Registry, nil] schema registry
65
+
66
+ attr_reader :registry
67
+
68
+
69
+ # Create a new Engine instance
70
+ #
71
+ # @param schema [Schema] the schema to generate from
72
+ # @param registry [Registry, nil] optional registry for Ref() lookups
73
+ # @param resolver [Resolver, nil] optional resolver instance to reuse (for cycle detection)
74
+ #
75
+ # @example
76
+ # engine = Engine.new(user_schema, registry: schema_registry)
77
+ #
78
+
79
+ def initialize(schema, registry: nil, resolver: nil)
80
+ @schema = schema
81
+ @registry = registry
82
+ @rng = nil
83
+ @faker_adapter = nil
84
+ @uniqueness = Uniqueness.new(schema_name: schema.name)
85
+
86
+ # Use provided resolver or create new one if registry is available
87
+ # Reusing resolver ensures @generating set is shared for cycle detection
88
+ @resolver = resolver || (registry ? Resolver.new(registry) : nil)
89
+
90
+ # Pre-compute observability flag at initialization to avoid repeated
91
+ # config lookups in the hot path (called once per Engine, not per record)
92
+ @observability_enabled = Synthra.configuration.observability_enabled?
93
+ end
94
+
95
+
96
+ # Generate a single record
97
+ #
98
+ # Creates one fake data record based on the schema definition.
99
+ # Use the seed parameter for deterministic/reproducible output.
100
+ #
101
+ # @param seed [Integer, nil] random seed for deterministic output
102
+ # @param mode [Symbol] generation mode (:random, :edge, :invalid, :mixed)
103
+ # @param overrides [Hash] field values to use instead of generating
104
+ # @return [Hash] the generated record
105
+ #
106
+ # @example Basic generation
107
+ # record = engine.generate
108
+ #
109
+ # @example Deterministic generation
110
+ # record = engine.generate(seed: 12345)
111
+ #
112
+ # @example With overrides
113
+ # record = engine.generate(overrides: { "name" => "Test User" })
114
+ #
115
+
116
+ def generate(seed: nil, mode: :random, overrides: {}, depth: 0, parent_context: nil, shared_context: nil)
117
+
118
+ # Reset RNG if seed is provided, or create one if none exists
119
+ setup_rng(seed) if seed || @rng.nil?
120
+ generate_single(mode: mode, overrides: overrides, depth: depth, parent_context: parent_context, shared_context: shared_context)
121
+ end
122
+
123
+
124
+ # Generate multiple records
125
+ #
126
+ # Creates multiple fake data records efficiently. Uniqueness
127
+ # constraints are enforced across the entire batch.
128
+ #
129
+ # @param count [Integer] number of records to generate
130
+ # @param seed [Integer, nil] random seed for deterministic output
131
+ # @param mode [Symbol] generation mode
132
+ # @param overrides [Hash] field values to override in all records
133
+ # @return [Array<Hash>] array of generated records
134
+ #
135
+ # @example Generate 100 users
136
+ # users = engine.generate_many(100)
137
+ #
138
+ # @example Deterministic batch
139
+ # users = engine.generate_many(100, seed: 42)
140
+ #
141
+ # @note For large batches (10K+ records), consider using
142
+ # generate_many_stream for memory-efficient lazy generation.
143
+ #
144
+
145
+ def generate_many(count, seed: nil, mode: :random, overrides: {}, shared_context: nil)
146
+ setup_rng(seed)
147
+ @uniqueness.clear # Reset uniqueness tracking for new batch
148
+
149
+ # Create shared context for batch - values persist across all records
150
+ shared_context ||= {}
151
+
152
+ count.times.map do
153
+ generate_single(mode: mode, overrides: overrides, depth: 0, shared_context: shared_context)
154
+ end
155
+ end
156
+
157
+
158
+ # Generate multiple records as a lazy stream (Enumerator)
159
+ #
160
+ # Returns an Enumerator that generates records on-demand. This is
161
+ # memory-efficient for large batches as records are not all held
162
+ # in memory at once.
163
+ #
164
+ # @param count [Integer] number of records to generate
165
+ # @param seed [Integer, nil] random seed for deterministic output
166
+ # @param mode [Symbol] generation mode
167
+ # @param overrides [Hash] field values to override in all records
168
+ # @return [Enumerator] lazy enumerator of records
169
+ #
170
+ # @example Generate 10K users efficiently
171
+ # engine.generate_many_stream(10000, seed: 42).each do |user|
172
+ # process(user) # Records generated one at a time
173
+ # end
174
+ #
175
+
176
+ def generate_many_stream(count, seed: nil, mode: :random, overrides: {}, shared_context: nil)
177
+ setup_rng(seed)
178
+ @uniqueness.clear # Reset uniqueness tracking for new batch
179
+
180
+ # Create shared context for batch - values persist across all records
181
+ shared_context ||= {}
182
+
183
+ Enumerator.new(count) do |yielder|
184
+ count.times do
185
+ yielder << generate_single(mode: mode, overrides: overrides, depth: 0, shared_context: shared_context)
186
+ end
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+
193
+ # Setup RNG and FakerAdapter for deterministic output
194
+ #
195
+ # @param seed [Integer, nil] seed value (nil generates random seed)
196
+ # @return [void]
197
+ #
198
+
199
+ def setup_rng(seed)
200
+ @rng = RNG.new(seed)
201
+ @faker_adapter = FakerAdapter.new(@rng)
202
+ end
203
+
204
+
205
+ # Generate a single record (internal)
206
+ #
207
+ # @param mode [Symbol] generation mode
208
+ # @param overrides [Hash] field overrides
209
+ # @param depth [Integer] current recursion depth
210
+ # @param shared_context [Hash] shared values that persist across batch
211
+ # @return [Hash] generated record
212
+ #
213
+
214
+ def generate_single(mode:, overrides:, depth: 0, parent_context: nil, shared_context: nil)
215
+ # Cache config locally to avoid repeated method calls
216
+ config = Synthra.configuration
217
+ presence_rate = config.optional_field_presence_rate || Synthra::OPTIONAL_FIELD_PRESENCE_RATE
218
+ null_rate = config.nullable_field_null_rate || Synthra::NULLABLE_FIELD_NULL_RATE
219
+
220
+ # Create context with proper initialization
221
+ context = Context.new(
222
+ {},
223
+ registry: @registry,
224
+ depth: depth,
225
+ parent_context: parent_context,
226
+ shared_context: shared_context || {},
227
+ resolver: @resolver,
228
+ faker_adapter: @faker_adapter
229
+ )
230
+
231
+ result = {}
232
+ applicator = Behaviors::Applicator.new(@schema, @rng)
233
+
234
+ # Iterate fields in definition order (important for copy() to work)
235
+ @schema.fields.each do |field|
236
+ # Skip if override provided
237
+ if overrides.key?(field.name) || overrides.key?(field.name.to_sym)
238
+ result[field.name] = overrides[field.name] || overrides[field.name.to_sym]
239
+ context[field.name] = result[field.name]
240
+ next
241
+ end
242
+
243
+ # Handle conditional fields
244
+ if field.conditional?
245
+ next unless context[field.condition]
246
+ end
247
+
248
+ # Handle optional fields
249
+ next if field.optional? && !@rng.boolean(presence_rate)
250
+
251
+ # Generate the field value with optional timing
252
+ start_time = @observability_enabled ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
253
+ value = generate_field(field, context, mode)
254
+
255
+ # Apply field-level behaviors
256
+ value = applicator.apply_field_behaviors(field, value)
257
+ next if value == :omit
258
+
259
+ # Handle nullable fields
260
+ value = nil if field.nullable? && @rng.boolean(null_rate)
261
+
262
+ # Observability hooks (only if enabled - checked once at init)
263
+ if @observability_enabled
264
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
265
+ emit_observability(config, field, value, duration_ms)
266
+ end
267
+
268
+ result[field.name] = value
269
+ context[field.name] = value
270
+ end
271
+
272
+ result
273
+ end
274
+
275
+ def emit_observability(config, field, value, duration_ms)
276
+ if config.metrics_collector
277
+ config.metrics_collector.timing(
278
+ "synthra.field_generation",
279
+ duration_ms,
280
+ schema: @schema.name,
281
+ field: field.name,
282
+ type: field.type_name
283
+ )
284
+ end
285
+
286
+ config.logger&.debug(
287
+ "Generated field #{@schema.name}.#{field.name} (#{field.type_name}) in #{duration_ms}ms"
288
+ )
289
+
290
+ config.on_field_generated&.call(@schema.name, field.name, value, duration_ms)
291
+ end
292
+
293
+
294
+ # Generate a single field value
295
+ #
296
+ # @param field [Field] the field definition
297
+ # @param context [Context] current generation context
298
+ # @param mode [Symbol] generation mode
299
+ # @return [Object] generated value
300
+ #
301
+
302
+ def generate_field(field, context, mode)
303
+ type_name = field.type_name
304
+ type_args = field.type_args
305
+
306
+
307
+ # Look up the type generator - first try Types::Registry
308
+ type_gen = begin
309
+ Types::Registry.lookup(type_name)
310
+ rescue Errors::UnknownTypeError
311
+ # If not a registered type, check if it's a schema reference
312
+ if @registry && @registry.schema?(type_name)
313
+ # Create an ObjectType for the schema reference
314
+ Types::Core::ObjectType.new(type_name)
315
+ else
316
+ raise # Re-raise the UnknownTypeError if not a schema either
317
+ end
318
+ end
319
+
320
+
321
+ # Pass faker_adapter in context for type generators to use
322
+ context.faker_adapter = @faker_adapter if @faker_adapter
323
+
324
+
325
+ # Handle uniqueness constraint
326
+ if field.unique?
327
+ @uniqueness.generate_unique(field.name) do
328
+ type_gen.generate(@rng, context, type_args, mode)
329
+ end
330
+ else
331
+ type_gen.generate(@rng, context, type_args, mode)
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end