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,718 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+ require "json"
5
+ require "uri"
6
+ require "openssl" # constant-time key comparison in #authenticate
7
+
8
+ module Synthra
9
+ # Production API Server
10
+ #
11
+ # A production-ready API server for data generation.
12
+ # Designed to be the core of your data generation platform.
13
+ #
14
+ # @example Start standalone server
15
+ # Synthra::APIServer.start(
16
+ # schema_dir: "schemas/",
17
+ # port: 3000,
18
+ # environment: :production
19
+ # )
20
+ #
21
+ # @example Use as Rack middleware
22
+ # # config.ru
23
+ # require 'synthra'
24
+ # run Synthra::APIServer.rack_app(schema_dir: "schemas/")
25
+ #
26
+ # @example With authentication
27
+ # Synthra::APIServer.start(
28
+ # schema_dir: "schemas/",
29
+ # auth: { type: :api_key, keys: ["secret-key-123"] }
30
+ # )
31
+ #
32
+ class APIServer
33
+ VERSION = "1.0.0"
34
+
35
+ attr_reader :registry, :config
36
+
37
+ # Server configuration defaults
38
+ DEFAULT_CONFIG = {
39
+ port: 3000,
40
+ host: "0.0.0.0",
41
+ environment: :development,
42
+ cors: true,
43
+ rate_limit: 0,
44
+ max_batch_size: 10_000,
45
+ max_stream_size: 1_000_000,
46
+ auth: nil,
47
+ log_level: :info,
48
+ request_timeout: 30,
49
+ enable_metrics: true,
50
+ enable_caching: false,
51
+ cache_ttl: 60
52
+ }.freeze
53
+
54
+ # Create a new API server
55
+ #
56
+ # @param schema_dir [String] directory containing DSL schemas
57
+ # @param config [Hash] server configuration
58
+ #
59
+ def initialize(schema_dir:, **config)
60
+ @config = DEFAULT_CONFIG.merge(config)
61
+ @registry = Registry.new
62
+ @registry.load_dir(schema_dir)
63
+ @metrics = Metrics.new if @config[:enable_metrics]
64
+ @cache = Cache.new(@config[:cache_ttl]) if @config[:enable_caching]
65
+ @rate_limiter = RateLimiter.new(@config[:rate_limit]) if @config[:rate_limit] > 0
66
+ end
67
+
68
+ # Start the server
69
+ def start
70
+ # :nocov: binds a real TCP port and blocks on WEBrick#start — exercised via integration use.
71
+ print_banner
72
+ setup_signal_handlers
73
+
74
+ @server = WEBrick::HTTPServer.new(
75
+ Port: @config[:port],
76
+ BindAddress: @config[:host],
77
+ Logger: create_logger,
78
+ AccessLog: []
79
+ )
80
+
81
+ @server.mount_proc("/", &method(:handle_request))
82
+ @server.start
83
+ # :nocov:
84
+ end
85
+
86
+ # Start in background
87
+ def start_async
88
+ # :nocov: spawns a thread that binds a port and sleeps — integration-only.
89
+ @thread = Thread.new { start }
90
+ sleep 0.5 # Wait for server to start
91
+ @thread
92
+ # :nocov:
93
+ end
94
+
95
+ # Stop the server
96
+ def stop
97
+ @server&.shutdown
98
+ end
99
+
100
+ # Rack application for use with Puma, etc.
101
+ def self.rack_app(schema_dir:, **config)
102
+ server = new(schema_dir: schema_dir, **config)
103
+
104
+ lambda do |env|
105
+ server.handle_rack_request(env)
106
+ end
107
+ end
108
+
109
+ # Handle Rack request
110
+ def handle_rack_request(env)
111
+ request = RackRequest.new(env)
112
+ response = RackResponse.new
113
+
114
+ handle_request_internal(request, response)
115
+
116
+ [response.status, response.headers, [response.body]]
117
+ end
118
+
119
+ class << self
120
+ def start(**options)
121
+ new(**options).start
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def print_banner
128
+ puts <<~BANNER
129
+
130
+ ╔══════════════════════════════════════════════════════════════════╗
131
+ ║ ║
132
+ ║ 🚀 Synthra API Server v#{VERSION} ║
133
+ ║ ║
134
+ ║ Production-Ready Data Generation API ║
135
+ ║ ║
136
+ ╠══════════════════════════════════════════════════════════════════╣
137
+ ║ ║
138
+ ║ Server: http://#{@config[:host]}:#{@config[:port]}#{" " * (29 - @config[:host].length - @config[:port].to_s.length)}║
139
+ ║ Environment: #{@config[:environment].to_s.ljust(42)}║
140
+ ║ Schemas: #{@registry.names.count.to_s.ljust(42)}║
141
+ ║ ║
142
+ ╚══════════════════════════════════════════════════════════════════╝
143
+
144
+ 📊 Available Schemas:
145
+ #{@registry.names.map { |n| " • #{n}" }.join("\n")}
146
+
147
+ 🔗 API Endpoints:
148
+ GET / Health check
149
+ GET /api/schemas List schemas
150
+ GET /api/:schema Generate record
151
+ GET /api/:schema/batch Generate batch
152
+ GET /api/:schema/stream Stream records (NDJSON)
153
+ POST /api/:schema Generate with overrides
154
+ GET /api/:schema/openapi OpenAPI spec for schema
155
+ GET /api/:schema/protobuf Protobuf spec for schema
156
+ GET /metrics Server metrics
157
+
158
+ Press Ctrl+C to stop
159
+
160
+ BANNER
161
+ end
162
+
163
+ def setup_signal_handlers
164
+ trap("INT") { stop }
165
+ trap("TERM") { stop }
166
+ end
167
+
168
+ def create_logger
169
+ WEBrick::Log.new(
170
+ $stderr,
171
+ @config[:log_level] == :debug ? WEBrick::Log::DEBUG : WEBrick::Log::INFO
172
+ )
173
+ end
174
+
175
+ def handle_request(req, res)
176
+ # :nocov: WEBrick mount_proc glue — wraps live WEBrick request/response objects.
177
+ request = WEBrickRequest.new(req)
178
+ response = WEBrickResponse.new(res)
179
+ handle_request_internal(request, response)
180
+ # :nocov:
181
+ end
182
+
183
+ def handle_request_internal(request, response)
184
+ start_time = Time.now
185
+
186
+ # CORS
187
+ add_cors_headers(response) if @config[:cors]
188
+
189
+ if request.options?
190
+ response.status = 204
191
+ return
192
+ end
193
+
194
+ # Authentication
195
+ if @config[:auth] && !authenticate(request)
196
+ response.status = 401
197
+ response.json({ error: "Unauthorized" })
198
+ return
199
+ end
200
+
201
+ # Rate limiting
202
+ if @rate_limiter && @rate_limiter.limited?(request.client_ip)
203
+ response.status = 429
204
+ response.json({ error: "Rate limit exceeded", retry_after: @rate_limiter.retry_after(request.client_ip) })
205
+ return
206
+ end
207
+
208
+ # Route
209
+ route(request, response)
210
+
211
+ # Metrics
212
+ if @metrics
213
+ @metrics.record(
214
+ path: request.path,
215
+ method: request.method,
216
+ status: response.status,
217
+ duration: Time.now - start_time
218
+ )
219
+ end
220
+ rescue StandardError => e
221
+ handle_error(e, response)
222
+ end
223
+
224
+ def route(request, response)
225
+ case request.path
226
+ when "/"
227
+ handle_health(response)
228
+ when "/api/schemas"
229
+ handle_list_schemas(response)
230
+ when "/metrics"
231
+ handle_metrics(response)
232
+ when %r{^/api/([^/]+)/openapi$}
233
+ handle_openapi(request, response, $1)
234
+ when %r{^/api/([^/]+)/protobuf$}
235
+ handle_protobuf(request, response, $1)
236
+ when %r{^/api/([^/]+)/batch$}
237
+ handle_batch(request, response, $1)
238
+ when %r{^/api/([^/]+)/stream$}
239
+ handle_stream(request, response, $1)
240
+ when %r{^/api/([^/]+)$}
241
+ if request.post?
242
+ handle_generate_post(request, response, $1)
243
+ else
244
+ handle_generate(request, response, $1)
245
+ end
246
+ else
247
+ response.status = 404
248
+ response.json({ error: "Not found", path: request.path })
249
+ end
250
+ end
251
+
252
+ def handle_health(response)
253
+ response.json({
254
+ status: "healthy",
255
+ version: VERSION,
256
+ schemas: @registry.names.count,
257
+ uptime: uptime
258
+ })
259
+ end
260
+
261
+ def handle_list_schemas(response)
262
+ response.json({
263
+ schemas: @registry.names.sort.map do |name|
264
+ schema = @registry.schema(name)
265
+ {
266
+ name: name,
267
+ fields: schema.fields.count,
268
+ endpoint: "/api/#{to_snake_case(name)}"
269
+ }
270
+ end
271
+ })
272
+ end
273
+
274
+ def handle_generate(request, response, schema_slug)
275
+ schema = find_schema(schema_slug)
276
+ return schema_not_found(response, schema_slug) unless schema
277
+
278
+ params = request.query_params
279
+ cache_key = "#{schema.name}:#{params[:seed]}:#{params[:mode]}"
280
+
281
+ if @cache && (cached = @cache.get(cache_key))
282
+ response.json(cached)
283
+ return
284
+ end
285
+
286
+ data = schema.generate(
287
+ seed: params[:seed]&.to_i,
288
+ mode: (params[:mode] || @config[:default_mode] || :random).to_sym,
289
+ registry: @registry
290
+ )
291
+
292
+ @cache&.set(cache_key, data)
293
+ response.json(data)
294
+ end
295
+
296
+ def handle_generate_post(request, response, schema_slug)
297
+ schema = find_schema(schema_slug)
298
+ return schema_not_found(response, schema_slug) unless schema
299
+
300
+ body = request.json_body || {}
301
+ count = [body["count"] || 1, @config[:max_batch_size]].min
302
+
303
+ if count > 1
304
+ data = schema.generate_many(
305
+ count,
306
+ seed: body["seed"]&.to_i,
307
+ mode: (body["mode"] || :random).to_sym,
308
+ overrides: body["overrides"] || {},
309
+ registry: @registry
310
+ )
311
+ else
312
+ data = schema.generate(
313
+ seed: body["seed"]&.to_i,
314
+ mode: (body["mode"] || :random).to_sym,
315
+ overrides: body["overrides"] || {},
316
+ registry: @registry
317
+ )
318
+ end
319
+
320
+ response.json(data)
321
+ end
322
+
323
+ def handle_batch(request, response, schema_slug)
324
+ schema = find_schema(schema_slug)
325
+ return schema_not_found(response, schema_slug) unless schema
326
+
327
+ params = request.query_params
328
+ count = [[params[:count]&.to_i || 10, 1].max, @config[:max_batch_size]].min
329
+
330
+ data = schema.generate_many(
331
+ count,
332
+ seed: params[:seed]&.to_i,
333
+ mode: (params[:mode] || :random).to_sym,
334
+ registry: @registry
335
+ )
336
+
337
+ response.json({
338
+ data: data,
339
+ meta: {
340
+ count: data.length,
341
+ schema: schema.name,
342
+ seed: params[:seed]&.to_i,
343
+ mode: params[:mode] || "random"
344
+ }
345
+ })
346
+ end
347
+
348
+ def handle_stream(request, response, schema_slug)
349
+ schema = find_schema(schema_slug)
350
+ return schema_not_found(response, schema_slug) unless schema
351
+
352
+ params = request.query_params
353
+ count = [[params[:count]&.to_i || 100, 1].max, @config[:max_stream_size]].min
354
+
355
+ response.content_type = "application/x-ndjson"
356
+
357
+ output = +""
358
+ schema.generate_stream(count: count, mode: (params[:mode] || :random).to_sym).each do |record|
359
+ output << JSON.generate(record) << "\n"
360
+ end
361
+ response.body = output
362
+ end
363
+
364
+ def handle_openapi(request, response, schema_slug)
365
+ schema = find_schema(schema_slug)
366
+ return schema_not_found(response, schema_slug) unless schema
367
+
368
+ # Create a mini registry with just this schema
369
+ mini_registry = Registry.new
370
+ mini_registry.register_schema(schema.name, schema)
371
+
372
+ exporter = Export::OpenAPI.new(mini_registry)
373
+ spec = exporter.export_as(request.query_params[:format]&.to_sym || :yaml)
374
+
375
+ response.content_type = request.query_params[:format] == "json" ? "application/json" : "application/yaml"
376
+ response.body = spec
377
+ end
378
+
379
+ def handle_protobuf(request, response, schema_slug)
380
+ schema = find_schema(schema_slug)
381
+ return schema_not_found(response, schema_slug) unless schema
382
+
383
+ exporter = Export::Protobuf.new(schema, registry: @registry)
384
+ proto = exporter.export
385
+
386
+ response.content_type = "text/plain"
387
+ response.body = proto
388
+ end
389
+
390
+ def handle_metrics(response)
391
+ if @metrics
392
+ response.json(@metrics.to_h)
393
+ else
394
+ response.json({ enabled: false })
395
+ end
396
+ end
397
+
398
+ def find_schema(slug)
399
+ # Try exact match first
400
+ return @registry.schema(slug) if @registry.schema?(slug)
401
+
402
+ # Try PascalCase
403
+ pascal = slug.split("_").map(&:capitalize).join
404
+ return @registry.schema(pascal) if @registry.schema?(pascal)
405
+
406
+ # Try case-insensitive match
407
+ @registry.names.find { |n| n.downcase == slug.downcase }&.then { |n| @registry.schema(n) }
408
+ end
409
+
410
+ def schema_not_found(response, slug)
411
+ response.status = 404
412
+ response.json({
413
+ error: "Schema not found",
414
+ requested: slug,
415
+ available: @registry.names.map { |n| to_snake_case(n) }
416
+ })
417
+ end
418
+
419
+ def authenticate(request)
420
+ return true unless @config[:auth]
421
+
422
+ case @config[:auth][:type]
423
+ when :api_key
424
+ # SECURITY: header only (never the query string — keys in URLs leak into access logs,
425
+ # browser history and Referer headers) and a constant-time compare (Array#include? leaks
426
+ # the key length/prefix via a timing oracle).
427
+ secure_match?(request.headers["X-API-Key"], @config[:auth][:keys])
428
+ when :bearer
429
+ auth = request.headers["Authorization"]
430
+ return false unless auth&.start_with?("Bearer ")
431
+
432
+ secure_match?(auth[7..], @config[:auth][:tokens])
433
+ else
434
+ true
435
+ end
436
+ end
437
+
438
+ # Constant-time membership test against a list of secrets. fixed_length_secure_compare raises on
439
+ # length mismatch, so the bytesize guard short-circuits non-matches without leaking timing.
440
+ def secure_match?(provided, allowed)
441
+ return false unless provided.is_a?(String) && allowed
442
+
443
+ allowed.any? do |candidate|
444
+ candidate.is_a?(String) &&
445
+ candidate.bytesize == provided.bytesize &&
446
+ OpenSSL.fixed_length_secure_compare(candidate, provided)
447
+ end
448
+ end
449
+
450
+ def add_cors_headers(response)
451
+ response.headers["Access-Control-Allow-Origin"] = "*"
452
+ response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
453
+ response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-API-Key"
454
+ response.headers["Access-Control-Max-Age"] = "86400"
455
+ end
456
+
457
+ def handle_error(error, response)
458
+ response.status = 500
459
+ response.json({
460
+ error: error.class.name,
461
+ message: error.message,
462
+ backtrace: @config[:environment] == :development ? error.backtrace&.first(10) : nil
463
+ })
464
+ end
465
+
466
+ def uptime
467
+ @start_time ||= Time.now
468
+ (Time.now - @start_time).round
469
+ end
470
+
471
+ def to_snake_case(str)
472
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
473
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
474
+ .downcase
475
+ end
476
+
477
+ # Simple metrics collector
478
+ class Metrics
479
+ def initialize
480
+ @requests = Hash.new(0)
481
+ @durations = Hash.new { |h, k| h[k] = [] }
482
+ @errors = Hash.new(0)
483
+ @mutex = Mutex.new
484
+ end
485
+
486
+ def record(path:, method:, status:, duration:)
487
+ @mutex.synchronize do
488
+ key = "#{method} #{path}"
489
+ @requests[key] += 1
490
+ @durations[key] << duration
491
+ @errors[key] += 1 if status >= 400
492
+ end
493
+ end
494
+
495
+ def to_h
496
+ @mutex.synchronize do
497
+ {
498
+ total_requests: @requests.values.sum,
499
+ endpoints: @requests.map do |key, count|
500
+ durations = @durations[key]
501
+ {
502
+ endpoint: key,
503
+ requests: count,
504
+ errors: @errors[key],
505
+ avg_duration_ms: (durations.sum / durations.length * 1000).round(2),
506
+ p99_duration_ms: (percentile(durations, 99) * 1000).round(2)
507
+ }
508
+ end
509
+ }
510
+ end
511
+ end
512
+
513
+ private
514
+
515
+ def percentile(array, p)
516
+ return 0 if array.empty?
517
+ sorted = array.sort
518
+ k = (p / 100.0 * (sorted.length - 1)).floor
519
+ sorted[k]
520
+ end
521
+ end
522
+
523
+ # Simple rate limiter
524
+ class RateLimiter
525
+ def initialize(limit_per_minute)
526
+ @limit = limit_per_minute
527
+ @counts = Hash.new { |h, k| h[k] = { count: 0, reset_at: Time.now + 60 } }
528
+ @mutex = Mutex.new
529
+ end
530
+
531
+ def limited?(ip)
532
+ @mutex.synchronize do
533
+ entry = @counts[ip]
534
+
535
+ if Time.now > entry[:reset_at]
536
+ entry[:count] = 0
537
+ entry[:reset_at] = Time.now + 60
538
+ end
539
+
540
+ entry[:count] += 1
541
+ entry[:count] > @limit
542
+ end
543
+ end
544
+
545
+ def retry_after(ip)
546
+ # Same mutex as #limited? — @counts has a default block, so even a read can mutate the hash.
547
+ @mutex.synchronize do
548
+ entry = @counts[ip]
549
+ [entry[:reset_at] - Time.now, 0].max.ceil
550
+ end
551
+ end
552
+ end
553
+
554
+ # Simple cache
555
+ class Cache
556
+ def initialize(ttl)
557
+ @ttl = ttl
558
+ @store = {}
559
+ @mutex = Mutex.new
560
+ end
561
+
562
+ def get(key)
563
+ @mutex.synchronize do
564
+ entry = @store[key]
565
+ return nil unless entry
566
+ return nil if Time.now > entry[:expires_at]
567
+ entry[:value]
568
+ end
569
+ end
570
+
571
+ def set(key, value)
572
+ @mutex.synchronize do
573
+ @store[key] = { value: value, expires_at: Time.now + @ttl }
574
+ end
575
+ end
576
+ end
577
+
578
+ # Request adapter for WEBrick
579
+ class WEBrickRequest
580
+ def initialize(req)
581
+ @req = req
582
+ end
583
+
584
+ def path
585
+ @req.path
586
+ end
587
+
588
+ def method
589
+ @req.request_method
590
+ end
591
+
592
+ def options?
593
+ method == "OPTIONS"
594
+ end
595
+
596
+ def post?
597
+ method == "POST"
598
+ end
599
+
600
+ def query_params
601
+ @req.query || {}
602
+ end
603
+
604
+ def headers
605
+ Hash[@req.header.map { |k, v| [k.tr("_", "-").downcase, v.first] }]
606
+ end
607
+
608
+ def client_ip
609
+ @req.peeraddr[2]
610
+ end
611
+
612
+ def json_body
613
+ JSON.parse(@req.body || "{}") rescue {}
614
+ end
615
+ end
616
+
617
+ # Response adapter for WEBrick
618
+ class WEBrickResponse
619
+ attr_accessor :status
620
+ attr_reader :headers
621
+
622
+ def initialize(res)
623
+ @res = res
624
+ @headers = {}
625
+ end
626
+
627
+ def status=(code)
628
+ @res.status = code
629
+ end
630
+
631
+ def status
632
+ @res.status
633
+ end
634
+
635
+ def content_type=(type)
636
+ @res["Content-Type"] = type
637
+ end
638
+
639
+ def body=(content)
640
+ @res.body = content
641
+ end
642
+
643
+ def json(data)
644
+ @res["Content-Type"] = "application/json"
645
+ @res.body = JSON.pretty_generate(data)
646
+ end
647
+
648
+ def headers
649
+ @res.header
650
+ end
651
+ end
652
+
653
+ # Request adapter for Rack
654
+ class RackRequest
655
+ def initialize(env)
656
+ @env = env
657
+ @request = Rack::Request.new(env) if defined?(Rack::Request)
658
+ end
659
+
660
+ def path
661
+ @env["PATH_INFO"]
662
+ end
663
+
664
+ def method
665
+ @env["REQUEST_METHOD"]
666
+ end
667
+
668
+ def options?
669
+ method == "OPTIONS"
670
+ end
671
+
672
+ def post?
673
+ method == "POST"
674
+ end
675
+
676
+ def query_params
677
+ return {} unless @request
678
+ @request.params.transform_keys(&:to_sym)
679
+ end
680
+
681
+ def headers
682
+ @env.select { |k, _| k.start_with?("HTTP_") }
683
+ .transform_keys { |k| k[5..].tr("_", "-").downcase }
684
+ end
685
+
686
+ def client_ip
687
+ @env["REMOTE_ADDR"]
688
+ end
689
+
690
+ def json_body
691
+ body = @env["rack.input"]&.read
692
+ @env["rack.input"]&.rewind
693
+ JSON.parse(body || "{}") rescue {}
694
+ end
695
+ end
696
+
697
+ # Response adapter for Rack
698
+ class RackResponse
699
+ attr_accessor :status, :body
700
+ attr_reader :headers
701
+
702
+ def initialize
703
+ @status = 200
704
+ @headers = { "Content-Type" => "application/json" }
705
+ @body = ""
706
+ end
707
+
708
+ def content_type=(type)
709
+ @headers["Content-Type"] = type
710
+ end
711
+
712
+ def json(data)
713
+ @headers["Content-Type"] = "application/json"
714
+ @body = JSON.pretty_generate(data)
715
+ end
716
+ end
717
+ end
718
+ end