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,458 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+ require "json"
5
+ require "fileutils"
6
+ require "monitor" # thread-safety for recordings + rate-limit counters (WEBrick = 1 thread/req)
7
+
8
+ module Synthra
9
+ # Production-Ready Mock Server with Recording
10
+ #
11
+ # A full-featured mock server that can:
12
+ # - Serve generated data via REST API
13
+ # - Record real API responses for replay
14
+ # - Support multiple response modes
15
+ # - Handle rate limiting and behaviors
16
+ #
17
+ # @example Start server
18
+ # Synthra::MockServer.start(schema_dir: "schemas/", port: 3000)
19
+ #
20
+ # @example With recording
21
+ # server = Synthra::MockServer.new(schema_dir: "schemas/", recording: true)
22
+ # server.start
23
+ #
24
+ class MockServer
25
+ attr_reader :registry, :options, :recordings
26
+
27
+ DEFAULT_PORT = 3000
28
+
29
+ # Create a new mock server
30
+ #
31
+ # @param schema_dir [String] directory containing DSL schemas
32
+ # @param options [Hash] server options
33
+ # @option options [Integer] :port Server port (default: 3000)
34
+ # @option options [Boolean] :recording Enable request recording
35
+ # @option options [String] :recordings_dir Directory for recordings
36
+ # @option options [Boolean] :cors Enable CORS headers
37
+ # @option options [Integer] :rate_limit Requests per minute (0 = unlimited)
38
+ # @option options [Symbol] :default_mode Default generation mode
39
+ #
40
+ def initialize(schema_dir:, **options)
41
+ @options = {
42
+ port: DEFAULT_PORT,
43
+ recording: false,
44
+ recordings_dir: "recordings",
45
+ cors: true,
46
+ rate_limit: 0,
47
+ default_mode: :random
48
+ }.merge(options)
49
+
50
+ @registry = Registry.new
51
+ @registry.load_dir(schema_dir)
52
+ @recordings = {}
53
+ @request_counts = Hash.new(0)
54
+ @last_reset = Time.now
55
+ # Monitor (reentrant) so handle_recording can call save_recordings without self-deadlock.
56
+ @mutex = Monitor.new
57
+
58
+ load_recordings if @options[:recording]
59
+ end
60
+
61
+ # Start the mock server
62
+ def start
63
+ # :nocov: binds a real TCP port and blocks on WEBrick#start — exercised via integration use.
64
+ puts banner
65
+ puts ""
66
+ puts "📊 Available schemas: #{@registry.names.sort.join(', ')}"
67
+ puts ""
68
+ puts "🔗 Endpoints:"
69
+ puts " GET /api/schemas - List all schemas"
70
+ puts " GET /api/:schema - Generate single record"
71
+ puts " GET /api/:schema/batch - Generate multiple records"
72
+ puts " GET /api/:schema/stream - Stream records (NDJSON)"
73
+ puts " POST /api/:schema - Generate with overrides"
74
+ puts " GET /api/:schema/schema - Get schema definition"
75
+ puts ""
76
+ if @options[:recording]
77
+ puts "📼 Recording mode: ENABLED"
78
+ puts " POST /api/recordings/:name - Save recording"
79
+ puts " GET /api/recordings/:name - Replay recording"
80
+ puts ""
81
+ end
82
+ puts "Press Ctrl+C to stop"
83
+ puts ""
84
+
85
+ @server = WEBrick::HTTPServer.new(
86
+ Port: @options[:port],
87
+ Logger: WEBrick::Log.new($stderr, WEBrick::Log::INFO),
88
+ AccessLog: [[File.open("/dev/null", "w"), WEBrick::AccessLog::COMBINED_LOG_FORMAT]]
89
+ )
90
+
91
+ setup_routes
92
+
93
+ trap("INT") { @server.shutdown }
94
+ @server.start
95
+ # :nocov:
96
+ end
97
+
98
+ # Start server in background thread
99
+ def start_async
100
+ # :nocov: spawns a thread that binds a port — integration-only.
101
+ Thread.new { start }
102
+ # :nocov:
103
+ end
104
+
105
+ # Stop the server
106
+ def stop
107
+ @server&.shutdown
108
+ end
109
+
110
+ class << self
111
+ def start(**options)
112
+ new(**options).start
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def banner
119
+ <<~BANNER
120
+ ╔═══════════════════════════════════════════════════════════════╗
121
+ ║ ║
122
+ ║ 🎲 Synthra Mock Server ║
123
+ ║ ║
124
+ ║ Production-ready data generation API ║
125
+ ║ http://localhost:#{@options[:port].to_s.ljust(5)} ║
126
+ ║ ║
127
+ ╚═══════════════════════════════════════════════════════════════╝
128
+ BANNER
129
+ end
130
+
131
+ def setup_routes
132
+ # CORS preflight
133
+ @server.mount_proc "/" do |req, res|
134
+ add_cors_headers(res) if @options[:cors]
135
+
136
+ if req.request_method == "OPTIONS"
137
+ res.status = 204
138
+ next
139
+ end
140
+
141
+ # Rate limiting
142
+ if @options[:rate_limit] > 0 && rate_limited?(req)
143
+ res.status = 429
144
+ res["Content-Type"] = "application/json"
145
+ res.body = JSON.generate({ error: "Rate limit exceeded" })
146
+ next
147
+ end
148
+
149
+ route_request(req, res)
150
+ end
151
+ end
152
+
153
+ def route_request(req, res)
154
+ path = req.path
155
+ method = req.request_method
156
+
157
+ case path
158
+ when "/api/schemas"
159
+ handle_list_schemas(req, res)
160
+ when %r{^/api/recordings/(.+)$}
161
+ handle_recording(req, res, $1)
162
+ when %r{^/api/([^/]+)/batch$}
163
+ handle_batch_generate(req, res, $1)
164
+ when %r{^/api/([^/]+)/stream$}
165
+ handle_stream_generate(req, res, $1)
166
+ when %r{^/api/([^/]+)/schema$}
167
+ handle_schema_info(req, res, $1)
168
+ when %r{^/api/([^/]+)$}
169
+ if method == "POST"
170
+ handle_generate_with_overrides(req, res, $1)
171
+ else
172
+ handle_generate(req, res, $1)
173
+ end
174
+ else
175
+ res.status = 404
176
+ res["Content-Type"] = "application/json"
177
+ res.body = JSON.generate({ error: "Not found", path: path })
178
+ end
179
+ end
180
+
181
+ def handle_list_schemas(_req, res)
182
+ res["Content-Type"] = "application/json"
183
+ res.body = JSON.pretty_generate({
184
+ schemas: @registry.names.sort,
185
+ count: @registry.names.count,
186
+ endpoints: @registry.names.map { |n| "/api/#{to_snake_case(n)}" }
187
+ })
188
+ end
189
+
190
+ def handle_generate(req, res, schema_name)
191
+ schema_name = normalize_schema_name(schema_name)
192
+
193
+ unless @registry.schema?(schema_name)
194
+ res.status = 404
195
+ res["Content-Type"] = "application/json"
196
+ res.body = JSON.generate({
197
+ error: "Schema not found",
198
+ schema: schema_name,
199
+ available: @registry.names
200
+ })
201
+ return
202
+ end
203
+
204
+ schema = @registry.schema(schema_name)
205
+ params = parse_query_params(req)
206
+
207
+ data = schema.generate(
208
+ seed: params[:seed],
209
+ mode: params[:mode] || @options[:default_mode],
210
+ registry: @registry
211
+ )
212
+
213
+ # Apply behaviors (latency, failure)
214
+ apply_behaviors(schema, res)
215
+
216
+ res["Content-Type"] = "application/json"
217
+ res.body = JSON.pretty_generate(data)
218
+
219
+ record_response(schema_name, data) if @options[:recording]
220
+ end
221
+
222
+ def handle_generate_with_overrides(req, res, schema_name)
223
+ schema_name = normalize_schema_name(schema_name)
224
+
225
+ unless @registry.schema?(schema_name)
226
+ res.status = 404
227
+ res["Content-Type"] = "application/json"
228
+ res.body = JSON.generate({ error: "Schema not found" })
229
+ return
230
+ end
231
+
232
+ schema = @registry.schema(schema_name)
233
+ body = JSON.parse(req.body || "{}") rescue {}
234
+
235
+ overrides = body["overrides"] || {}
236
+ count = body["count"] || 1
237
+ seed = body["seed"]
238
+ mode = (body["mode"] || @options[:default_mode]).to_sym
239
+
240
+ if count > 1
241
+ data = schema.generate_many(count, seed: seed, mode: mode, registry: @registry, overrides: overrides)
242
+ else
243
+ data = schema.generate(seed: seed, mode: mode, registry: @registry, overrides: overrides)
244
+ end
245
+
246
+ apply_behaviors(schema, res)
247
+
248
+ res["Content-Type"] = "application/json"
249
+ res.body = JSON.pretty_generate(data)
250
+ end
251
+
252
+ def handle_batch_generate(req, res, schema_name)
253
+ schema_name = normalize_schema_name(schema_name)
254
+
255
+ unless @registry.schema?(schema_name)
256
+ res.status = 404
257
+ res["Content-Type"] = "application/json"
258
+ res.body = JSON.generate({ error: "Schema not found" })
259
+ return
260
+ end
261
+
262
+ schema = @registry.schema(schema_name)
263
+ params = parse_query_params(req)
264
+ count = [params[:count] || 10, 10000].min
265
+
266
+ data = schema.generate_many(
267
+ count,
268
+ seed: params[:seed],
269
+ mode: params[:mode] || @options[:default_mode],
270
+ registry: @registry
271
+ )
272
+
273
+ apply_behaviors(schema, res)
274
+
275
+ res["Content-Type"] = "application/json"
276
+ res.body = JSON.pretty_generate({
277
+ data: data,
278
+ meta: {
279
+ count: data.length,
280
+ seed: params[:seed],
281
+ mode: (params[:mode] || @options[:default_mode]).to_s,
282
+ schema: schema_name
283
+ }
284
+ })
285
+ end
286
+
287
+ def handle_stream_generate(req, res, schema_name)
288
+ schema_name = normalize_schema_name(schema_name)
289
+
290
+ unless @registry.schema?(schema_name)
291
+ res.status = 404
292
+ res["Content-Type"] = "application/json"
293
+ res.body = JSON.generate({ error: "Schema not found" })
294
+ return
295
+ end
296
+
297
+ schema = @registry.schema(schema_name)
298
+ params = parse_query_params(req)
299
+ count = [params[:count] || 100, 100000].min
300
+
301
+ res["Content-Type"] = "application/x-ndjson"
302
+ res["Transfer-Encoding"] = "chunked"
303
+
304
+ # Stream NDJSON
305
+ output = +""
306
+ schema.generate_stream(count: count, mode: params[:mode] || @options[:default_mode]).each do |record|
307
+ output << JSON.generate(record) << "\n"
308
+ end
309
+ res.body = output
310
+ end
311
+
312
+ def handle_schema_info(_req, res, schema_name)
313
+ schema_name = normalize_schema_name(schema_name)
314
+
315
+ unless @registry.schema?(schema_name)
316
+ res.status = 404
317
+ res["Content-Type"] = "application/json"
318
+ res.body = JSON.generate({ error: "Schema not found" })
319
+ return
320
+ end
321
+
322
+ schema = @registry.schema(schema_name)
323
+
324
+ res["Content-Type"] = "application/json"
325
+ res.body = JSON.pretty_generate({
326
+ name: schema.name,
327
+ fields: schema.fields.map do |f|
328
+ {
329
+ name: f.name,
330
+ type: f.type_name,
331
+ optional: f.optional?,
332
+ nullable: f.nullable?,
333
+ args: f.type_args
334
+ }
335
+ end,
336
+ behaviors: schema.behaviors,
337
+ version: schema.respond_to?(:version) ? schema.version : nil
338
+ })
339
+ end
340
+
341
+ def handle_recording(req, res, name)
342
+ @mutex.synchronize do
343
+ if req.request_method == "POST"
344
+ # Save recording
345
+ body = JSON.parse(req.body || "{}") rescue {}
346
+ @recordings[name] = {
347
+ data: body["data"],
348
+ recorded_at: Time.now.iso8601,
349
+ content_type: body["content_type"] || "application/json"
350
+ }
351
+ save_recordings
352
+
353
+ res["Content-Type"] = "application/json"
354
+ res.body = JSON.generate({ saved: name })
355
+ elsif @recordings[name]
356
+ recording = @recordings[name]
357
+ res["Content-Type"] = recording["content_type"] || "application/json"
358
+ res.body = recording["data"].is_a?(String) ? recording["data"] : JSON.generate(recording["data"])
359
+ else
360
+ res.status = 404
361
+ res["Content-Type"] = "application/json"
362
+ res.body = JSON.generate({ error: "Recording not found", name: name })
363
+ end
364
+ end
365
+ end
366
+
367
+ def parse_query_params(req)
368
+ query = req.query || {}
369
+ {
370
+ seed: query["seed"]&.to_i,
371
+ mode: query["mode"]&.to_sym,
372
+ count: query["count"]&.to_i
373
+ }
374
+ end
375
+
376
+ def normalize_schema_name(name)
377
+ # Convert snake_case to PascalCase
378
+ name.split("_").map(&:capitalize).join
379
+ end
380
+
381
+ def apply_behaviors(schema, res)
382
+ schema.behaviors.each do |behavior|
383
+ case behavior[:type]
384
+ when :latency
385
+ min_ms = behavior[:min_ms] || 0
386
+ max_ms = behavior[:max_ms] || min_ms
387
+ delay = rand(min_ms..max_ms) / 1000.0
388
+ sleep(delay)
389
+ when :failure
390
+ if rand < (behavior[:rate] || 0)
391
+ res.status = behavior[:status] || 500
392
+ throw :response_sent
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ def add_cors_headers(res)
399
+ res["Access-Control-Allow-Origin"] = "*"
400
+ res["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
401
+ res["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
402
+ res["Access-Control-Max-Age"] = "86400"
403
+ end
404
+
405
+ def rate_limited?(req)
406
+ client_ip = req.peeraddr[2]
407
+ @mutex.synchronize do
408
+ # Reset counts every minute
409
+ if Time.now - @last_reset > 60
410
+ @request_counts.clear
411
+ @last_reset = Time.now
412
+ end
413
+
414
+ @request_counts[client_ip] += 1
415
+ @request_counts[client_ip] > @options[:rate_limit]
416
+ end
417
+ end
418
+
419
+ def record_response(schema_name, data)
420
+ @mutex.synchronize do
421
+ @recordings["#{schema_name}_#{Time.now.to_i}"] = {
422
+ data: data,
423
+ recorded_at: Time.now.iso8601,
424
+ schema: schema_name
425
+ }
426
+ end
427
+ end
428
+
429
+ def load_recordings
430
+ dir = @options[:recordings_dir]
431
+ return unless Dir.exist?(dir)
432
+
433
+ @mutex.synchronize do
434
+ Dir.glob(File.join(dir, "*.json")).each do |file|
435
+ name = File.basename(file, ".json")
436
+ @recordings[name] = JSON.parse(File.read(file))
437
+ end
438
+ end
439
+ end
440
+
441
+ def save_recordings
442
+ dir = @options[:recordings_dir]
443
+ FileUtils.mkdir_p(dir)
444
+
445
+ @mutex.synchronize do
446
+ @recordings.each do |name, data|
447
+ File.write(File.join(dir, "#{name}.json"), JSON.pretty_generate(data))
448
+ end
449
+ end
450
+ end
451
+
452
+ def to_snake_case(str)
453
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
454
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
455
+ .downcase
456
+ end
457
+ end
458
+ end