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,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra String Distance Utilities
6
+ # =============================================================================
7
+ #
8
+ # Provides string distance algorithms for "did you mean" suggestions in
9
+ # error messages. The primary algorithm is Levenshtein distance.
10
+ #
11
+ # @example Find similar strings
12
+ # StringDistance.levenshtein("uuid", "ulid") # => 2
13
+ # StringDistance.similar_strings("uui", ["uuid", "ulid", "name"], max_distance: 2)
14
+ # # => ["uuid", "ulid"]
15
+ #
16
+ # =============================================================================
17
+
18
+
19
+ module Synthra
20
+ module Utils
21
+
22
+ # String distance calculations for "did you mean" suggestions
23
+ #
24
+ # Provides methods to calculate edit distance between strings and
25
+ # find similar strings from a collection.
26
+ #
27
+ # @example Find similar type names
28
+ # StringDistance.similar_strings("strig", Synthra::Types::Registry.names)
29
+ # # => ["string", "text"]
30
+ #
31
+
32
+ module StringDistance
33
+ class << self
34
+ # Calculate Levenshtein distance between two strings
35
+ #
36
+ # The Levenshtein distance is the minimum number of single-character
37
+ # edits (insertions, deletions, or substitutions) required to
38
+ # transform one string into another.
39
+ #
40
+ # @param str1 [String] first string
41
+ # @param str2 [String] second string
42
+ # @return [Integer] the edit distance
43
+ #
44
+ # @example
45
+ # StringDistance.levenshtein("uuid", "ulid") # => 2
46
+ # StringDistance.levenshtein("text", "text") # => 0
47
+ # StringDistance.levenshtein("", "abc") # => 3
48
+ #
49
+ def levenshtein(str1, str2)
50
+ str1 = str1.to_s.downcase
51
+ str2 = str2.to_s.downcase
52
+
53
+ return str2.length if str1.empty?
54
+ return str1.length if str2.empty?
55
+ return 0 if str1 == str2
56
+
57
+ # Create matrix
58
+ m = str1.length
59
+ n = str2.length
60
+ d = Array.new(m + 1) { Array.new(n + 1) }
61
+
62
+ # Initialize first column
63
+ (0..m).each { |i| d[i][0] = i }
64
+
65
+ # Initialize first row
66
+ (0..n).each { |j| d[0][j] = j }
67
+
68
+ # Fill in the rest of the matrix
69
+ (1..m).each do |i|
70
+ (1..n).each do |j|
71
+ cost = str1[i - 1] == str2[j - 1] ? 0 : 1
72
+ d[i][j] = [
73
+ d[i - 1][j] + 1, # deletion
74
+ d[i][j - 1] + 1, # insertion
75
+ d[i - 1][j - 1] + cost # substitution
76
+ ].min
77
+ end
78
+ end
79
+
80
+ d[m][n]
81
+ end
82
+
83
+ # Find strings similar to a target from a collection
84
+ #
85
+ # Returns strings that are within the specified edit distance of
86
+ # the target, sorted by distance (closest first).
87
+ #
88
+ # @param target [String] the string to match
89
+ # @param candidates [Array<String>] collection to search
90
+ # @param max_distance [Integer] maximum edit distance (default: 3)
91
+ # @param limit [Integer] maximum number of results (default: 3)
92
+ # @return [Array<String>] similar strings, sorted by distance
93
+ #
94
+ # @example
95
+ # StringDistance.similar_strings("uui", ["uuid", "ulid", "name"])
96
+ # # => ["uuid"]
97
+ #
98
+ # StringDistance.similar_strings("tex", ["text", "next", "email"], max_distance: 2)
99
+ # # => ["text", "next"]
100
+ #
101
+ def similar_strings(target, candidates, max_distance: 3, limit: 3)
102
+ target = target.to_s
103
+
104
+ candidates
105
+ .map { |c| [c, levenshtein(target, c)] }
106
+ .select { |_, dist| dist <= max_distance }
107
+ .sort_by { |_, dist| dist }
108
+ .first(limit)
109
+ .map(&:first)
110
+ end
111
+
112
+ # Format "did you mean" suggestion text
113
+ #
114
+ # @param suggestions [Array<String>] list of suggestions
115
+ # @return [String, nil] formatted suggestion text or nil if empty
116
+ #
117
+ # @example
118
+ # StringDistance.format_suggestions(["uuid", "ulid"])
119
+ # # => "Did you mean: uuid, ulid?"
120
+ #
121
+ # StringDistance.format_suggestions([])
122
+ # # => nil
123
+ #
124
+ def format_suggestions(suggestions)
125
+ return nil if suggestions.nil? || suggestions.empty?
126
+
127
+ "Did you mean: #{suggestions.join(", ")}?"
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ # =============================================================================
6
+ #
7
+ # Synthra Simulyra DSL Validator
8
+ # =============================================================================
9
+ #
10
+ # Validates the Simulyra DSL structure according to the strict separation
11
+ # rules between Schema, API, and Scenario layers.
12
+ #
13
+ # Validation Rules:
14
+ # - No duplicate schema names
15
+ # - All schema(:Name) references resolve to existing schemas
16
+ # - APIs have at least one scenario
17
+ # - Scenarios have @status directive
18
+ # - Probability sum does not exceed 100%
19
+ # - No HTTP directives in schema blocks
20
+ # - No schema field definitions in API blocks
21
+ #
22
+ # @example Validate a ProgramNode
23
+ # validator = DslValidator.new(program_node)
24
+ # validator.validate!
25
+ #
26
+ # =============================================================================
27
+
28
+ module Synthra
29
+ module Validator
30
+ # Validates Simulyra DSL AST structure
31
+ #
32
+ # The DslValidator checks that the DSL follows the strict separation
33
+ # between Schema, API, and Scenario layers.
34
+ #
35
+ # @example Validate all rules
36
+ # validator = DslValidator.new(program_node)
37
+ # errors = validator.validate # Returns array of ValidationError
38
+ #
39
+ # @example Raise on first error
40
+ # validator = DslValidator.new(program_node)
41
+ # validator.validate! # Raises ValidationError
42
+ #
43
+ class DslValidator
44
+ # HTTP-related directives that are only valid in scenarios
45
+ HTTP_DIRECTIVES = %w[status probability latency header method].freeze
46
+
47
+ # Valid HTTP status codes
48
+ VALID_STATUS_CODES = (100..599).freeze
49
+
50
+ # @param program [Parser::AST::ProgramNode] the AST to validate
51
+ def initialize(program)
52
+ @program = program
53
+ @errors = []
54
+ @schema_names = Set.new
55
+ end
56
+
57
+ # Validate the DSL and return all errors
58
+ #
59
+ # @return [Array<Errors::ValidationError>] list of validation errors
60
+ #
61
+ def validate
62
+ @errors = []
63
+ @schema_names = Set.new
64
+
65
+ # Collect all schema names first (for reference validation)
66
+ collect_schema_names
67
+
68
+ # Run all validation rules
69
+ validate_no_duplicate_schemas
70
+ validate_schema_references
71
+ validate_apis_have_scenarios
72
+ validate_scenarios_have_status
73
+ validate_probability_sums
74
+ validate_schema_context
75
+ validate_api_context
76
+
77
+ @errors
78
+ end
79
+
80
+ # Validate and raise on first error
81
+ #
82
+ # @raise [Errors::ValidationError] if any validation fails
83
+ # @return [true] if all validations pass
84
+ #
85
+ def validate!
86
+ errors = validate
87
+ raise errors.first if errors.any?
88
+ true
89
+ end
90
+
91
+ private
92
+
93
+ # Collect all schema names for reference validation
94
+ #
95
+ # @return [void]
96
+ #
97
+ def collect_schema_names
98
+ @program.schemas.each do |schema|
99
+ @schema_names.add(schema.name)
100
+ end
101
+
102
+ # Also collect legacy schema names
103
+ @program.legacy_schemas.each do |schema|
104
+ @schema_names.add(schema.name)
105
+ end
106
+ end
107
+
108
+ # Validate no duplicate schema names
109
+ #
110
+ # @return [void]
111
+ #
112
+ def validate_no_duplicate_schemas
113
+ seen = {}
114
+
115
+ all_schemas = @program.schemas + @program.legacy_schemas
116
+ all_schemas.each do |schema|
117
+ if seen.key?(schema.name)
118
+ @errors << Errors::SchemaError.new(
119
+ "Duplicate schema \"#{schema.name}\"",
120
+ line: schema.line,
121
+ suggestion: "Defined again at line #{schema.line}. Schema names must be unique."
122
+ )
123
+ else
124
+ seen[schema.name] = schema
125
+ end
126
+ end
127
+ end
128
+
129
+ # Validate that all schema(:Name) references resolve
130
+ #
131
+ # @return [void]
132
+ #
133
+ def validate_schema_references
134
+ # Check schema references in schemas
135
+ all_schemas = @program.schemas + @program.legacy_schemas
136
+ all_schemas.each do |schema|
137
+ schema.fields.each do |field|
138
+ validate_type_references(field.type_node, schema.name)
139
+ end
140
+ end
141
+
142
+ # Check schema references in API scenarios
143
+ @program.apis.each do |api|
144
+ api.scenarios.each do |scenario|
145
+ scenario.response_fields.each do |field|
146
+ validate_type_references(field.type_node, "API #{api.method.upcase} #{api.path}")
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ # Validate type node for schema references
153
+ #
154
+ # @param type_node [Parser::AST::TypeNode] the type to validate
155
+ # @param context [String] context for error message
156
+ # @return [void]
157
+ #
158
+ def validate_type_references(type_node, context)
159
+ return unless type_node.is_a?(Parser::AST::SchemaReferenceNode)
160
+
161
+ unless @schema_names.include?(type_node.schema_name)
162
+ similar = find_similar_names(type_node.schema_name, @schema_names.to_a)
163
+ suggestion = similar.any? ? "Did you mean: #{similar.join(', ')}?" : nil
164
+
165
+ @errors << Errors::ReferenceError.new(
166
+ "Schema \"#{type_node.schema_name}\" is not defined",
167
+ line: type_node.line,
168
+ suggestion: "#{suggestion} Used in #{context}."
169
+ )
170
+ end
171
+ end
172
+
173
+ # Validate that APIs have at least one scenario
174
+ #
175
+ # @return [void]
176
+ #
177
+ def validate_apis_have_scenarios
178
+ @program.apis.each do |api|
179
+ if api.scenarios.empty?
180
+ @errors << Errors::ApiError.new(
181
+ "API #{api.method.upcase} #{api.path} must define at least one scenario",
182
+ line: api.line,
183
+ suggestion: "Add a scenario block with @status directive."
184
+ )
185
+ end
186
+ end
187
+ end
188
+
189
+ # Validate that scenarios have @status directive
190
+ #
191
+ # @return [void]
192
+ #
193
+ def validate_scenarios_have_status
194
+ @program.apis.each do |api|
195
+ api.scenarios.each do |scenario|
196
+ unless scenario.status_code
197
+ @errors << Errors::ScenarioError.new(
198
+ "Scenario \"#{scenario.name}\" must define @status",
199
+ line: scenario.line,
200
+ suggestion: "Required for HTTP simulation. Example: @status 200"
201
+ )
202
+ end
203
+
204
+ # Also validate status code is valid
205
+ if scenario.status_code && !VALID_STATUS_CODES.include?(scenario.status_code)
206
+ @errors << Errors::ScenarioError.new(
207
+ "Invalid status code #{scenario.status_code} in scenario \"#{scenario.name}\"",
208
+ line: scenario.line,
209
+ suggestion: "Status code must be between 100 and 599."
210
+ )
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ # Validate that probability sums don't exceed 100%
217
+ #
218
+ # @return [void]
219
+ #
220
+ def validate_probability_sums
221
+ @program.apis.each do |api|
222
+ total = 0
223
+ api.scenarios.each do |scenario|
224
+ total += scenario.probability if scenario.probability
225
+ end
226
+
227
+ if total > 100
228
+ @errors << Errors::ScenarioError.new(
229
+ "Total scenario probability exceeds 100%",
230
+ line: api.line,
231
+ suggestion: "Current total: #{total}% in API #{api.method.upcase} #{api.path}."
232
+ )
233
+ end
234
+ end
235
+ end
236
+
237
+ # Validate schema context (no HTTP directives in schemas)
238
+ #
239
+ # @return [void]
240
+ #
241
+ def validate_schema_context
242
+ all_schemas = @program.schemas + @program.legacy_schemas
243
+ all_schemas.each do |schema|
244
+ schema.behaviors.each do |behavior|
245
+ if HTTP_DIRECTIVES.include?(behavior.name.to_s)
246
+ @errors << Errors::ContextError.new(
247
+ "HTTP directives are not allowed inside schema definitions",
248
+ line: behavior.line,
249
+ suggestion: "@#{behavior.name} is an HTTP directive. Use it in scenario blocks only."
250
+ )
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ # Validate API context (scenarios should be properly nested)
257
+ #
258
+ # @return [void]
259
+ #
260
+ def validate_api_context
261
+ @program.apis.each do |api|
262
+ # Check for duplicate scenario names within an API
263
+ seen_scenarios = {}
264
+ api.scenarios.each do |scenario|
265
+ if seen_scenarios.key?(scenario.name)
266
+ @errors << Errors::ScenarioError.new(
267
+ "Duplicate scenario name \"#{scenario.name}\" in API #{api.method.upcase} #{api.path}",
268
+ line: scenario.line,
269
+ suggestion: "Scenario names must be unique within an API."
270
+ )
271
+ else
272
+ seen_scenarios[scenario.name] = scenario
273
+ end
274
+ end
275
+
276
+ # Check for multiple @status directives in a scenario
277
+ api.scenarios.each do |scenario|
278
+ status_count = scenario.directives.count { |d| d.is_a?(Parser::AST::StatusDirective) }
279
+ if status_count > 1
280
+ @errors << Errors::ScenarioError.new(
281
+ "Multiple @status directives are not allowed",
282
+ line: scenario.line,
283
+ suggestion: "First defined in scenario \"#{scenario.name}\". Use only one @status per scenario."
284
+ )
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ # Find similar names using basic string distance
291
+ #
292
+ # @param target [String] the name to match
293
+ # @param candidates [Array<String>] possible matches
294
+ # @return [Array<String>] similar names
295
+ #
296
+ def find_similar_names(target, candidates, max_results: 3)
297
+ scored = candidates.map do |name|
298
+ [name, levenshtein_distance(target.downcase, name.downcase)]
299
+ end
300
+
301
+ scored.select { |_, dist| dist <= 3 }
302
+ .sort_by { |_, dist| dist }
303
+ .first(max_results)
304
+ .map(&:first)
305
+ end
306
+
307
+ # Simple Levenshtein distance implementation
308
+ #
309
+ # @param s1 [String] first string
310
+ # @param s2 [String] second string
311
+ # @return [Integer] edit distance
312
+ #
313
+ def levenshtein_distance(s1, s2)
314
+ m = s1.length
315
+ n = s2.length
316
+ return n if m.zero?
317
+ return m if n.zero?
318
+
319
+ d = Array.new(m + 1) { Array.new(n + 1, 0) }
320
+
321
+ (0..m).each { |i| d[i][0] = i }
322
+ (0..n).each { |j| d[0][j] = j }
323
+
324
+ (1..m).each do |i|
325
+ (1..n).each do |j|
326
+ cost = s1[i - 1] == s2[j - 1] ? 0 : 1
327
+ d[i][j] = [
328
+ d[i - 1][j] + 1, # deletion
329
+ d[i][j - 1] + 1, # insertion
330
+ d[i - 1][j - 1] + cost # substitution
331
+ ].min
332
+ end
333
+ end
334
+
335
+ d[m][n]
336
+ end
337
+ end
338
+ end
339
+ end