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,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Schema Versioning Support
5
+ #
6
+ # Track schema versions and migration paths for API evolution.
7
+ #
8
+ # @example DSL syntax
9
+ # User@v1:
10
+ # id: uuid
11
+ # name: name
12
+ #
13
+ # User@v2:
14
+ # @migrated_from User@v1
15
+ # id: uuid
16
+ # first_name: name
17
+ # last_name: name
18
+ #
19
+ module SchemaVersioning
20
+ # Parse version from schema name
21
+ #
22
+ # @param name [String] schema name with optional version
23
+ # @return [Hash] { name: String, version: String|nil }
24
+ #
25
+ # @example
26
+ # parse_version("User@v2")
27
+ # # => { name: "User", version: "v2" }
28
+ #
29
+ def self.parse_version(name)
30
+ if name.include?("@")
31
+ parts = name.split("@", 2)
32
+ { name: parts[0], version: parts[1] }
33
+ else
34
+ { name: name, version: nil }
35
+ end
36
+ end
37
+
38
+ # Generate schema key for registry
39
+ #
40
+ # @param name [String] schema name
41
+ # @param version [String, nil] version
42
+ # @return [String] registry key
43
+ #
44
+ def self.registry_key(name, version = nil)
45
+ version ? "#{name}@#{version}" : name
46
+ end
47
+
48
+ # Version registry for tracking migrations
49
+ class Registry
50
+ class << self
51
+ def instance
52
+ @instance ||= new
53
+ end
54
+
55
+ def register(from:, to:, changes: {})
56
+ instance.register(from: from, to: to, changes: changes)
57
+ end
58
+
59
+ def get_migration(from, to)
60
+ instance.get_migration(from, to)
61
+ end
62
+
63
+ def migration_path(from, to)
64
+ instance.migration_path(from, to)
65
+ end
66
+
67
+ def versions_for(schema_name)
68
+ instance.versions_for(schema_name)
69
+ end
70
+
71
+ def clear
72
+ instance.clear
73
+ end
74
+ end
75
+
76
+ def initialize
77
+ @migrations = {}
78
+ @versions = Hash.new { |h, k| h[k] = [] }
79
+ @mutex = Mutex.new
80
+ end
81
+
82
+ # Register a migration from one version to another
83
+ #
84
+ # @param from [String] source version (e.g., "User@v1")
85
+ # @param to [String] target version (e.g., "User@v2")
86
+ # @param changes [Hash] migration changes
87
+ #
88
+ def register(from:, to:, changes: {})
89
+ @mutex.synchronize do
90
+ @migrations[[from, to]] = changes
91
+
92
+ # Track versions
93
+ from_parsed = SchemaVersioning.parse_version(from)
94
+ to_parsed = SchemaVersioning.parse_version(to)
95
+
96
+ @versions[from_parsed[:name]] << from_parsed[:version]
97
+ @versions[to_parsed[:name]] << to_parsed[:version]
98
+ @versions[from_parsed[:name]].uniq!
99
+ @versions[to_parsed[:name]].uniq!
100
+ end
101
+ end
102
+
103
+ # Get migration between two versions
104
+ #
105
+ # @param from [String] source version
106
+ # @param to [String] target version
107
+ # @return [Hash, nil] migration info
108
+ #
109
+ def get_migration(from, to)
110
+ @mutex.synchronize { @migrations[[from, to]] }
111
+ end
112
+
113
+ # Get all versions for a schema
114
+ #
115
+ # @param schema_name [String] schema name without version
116
+ # @return [Array<String>] versions
117
+ #
118
+ def versions_for(schema_name)
119
+ @mutex.synchronize { @versions[schema_name].sort }
120
+ end
121
+
122
+ # Find migration path between versions
123
+ #
124
+ # @param from [String] source version
125
+ # @param to [String] target version
126
+ # @return [Array<Array>] path of migrations
127
+ #
128
+ def migration_path(from, to)
129
+ @mutex.synchronize do
130
+ # Simple BFS to find path
131
+ visited = Set.new
132
+ queue = [[from, []]]
133
+
134
+ while queue.any?
135
+ current, path = queue.shift
136
+ return path if current == to
137
+ next if visited.include?(current)
138
+
139
+ visited.add(current)
140
+
141
+ @migrations.keys.each do |from_key, to_key|
142
+ if from_key == current && !visited.include?(to_key)
143
+ queue << [to_key, path + [[from_key, to_key]]]
144
+ end
145
+ end
146
+ end
147
+
148
+ nil # No path found
149
+ end
150
+ end
151
+
152
+ def clear
153
+ @mutex.synchronize do
154
+ @migrations.clear
155
+ @versions.clear
156
+ end
157
+ end
158
+ end
159
+
160
+ # Migration analyzer
161
+ class Analyzer
162
+ # Detect breaking changes between two schemas
163
+ #
164
+ # @param old_schema [Schema] old version
165
+ # @param new_schema [Schema] new version
166
+ # @return [Hash] analysis results
167
+ #
168
+ def self.analyze(old_schema, new_schema)
169
+ old_fields = old_schema.fields.map(&:name)
170
+ new_fields = new_schema.fields.map(&:name)
171
+
172
+ removed_fields = old_fields - new_fields
173
+ added_fields = new_fields - old_fields
174
+ common_fields = old_fields & new_fields
175
+
176
+ type_changes = []
177
+ common_fields.each do |name|
178
+ old_field = old_schema.field(name)
179
+ new_field = new_schema.field(name)
180
+
181
+ if old_field.type_name != new_field.type_name
182
+ type_changes << {
183
+ field: name,
184
+ from: old_field.type_name,
185
+ to: new_field.type_name
186
+ }
187
+ end
188
+ end
189
+
190
+ {
191
+ breaking: removed_fields.any? || type_changes.any?,
192
+ removed_fields: removed_fields,
193
+ added_fields: added_fields,
194
+ type_changes: type_changes,
195
+ summary: generate_summary(removed_fields, added_fields, type_changes)
196
+ }
197
+ end
198
+
199
+ private
200
+
201
+ def self.generate_summary(removed, added, type_changes)
202
+ parts = []
203
+ parts << "Removed: #{removed.join(', ')}" if removed.any?
204
+ parts << "Added: #{added.join(', ')}" if added.any?
205
+ type_changes.each do |change|
206
+ parts << "Changed #{change[:field]}: #{change[:from]} → #{change[:to]}"
207
+ end
208
+ parts.join("\n")
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Snapshot Testing Support
5
+ #
6
+ # Provides helpers for snapshot testing generated data.
7
+ # Ensures schema changes are detected and intentional.
8
+ #
9
+ # @example RSpec integration
10
+ # RSpec.describe "Schema Snapshots" do
11
+ # include Synthra::SnapshotTesting
12
+ #
13
+ # Synthra.each_schema do |schema|
14
+ # it "#{schema.name} generates stable output" do
15
+ # expect_snapshot(schema, seed: 42)
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ module SnapshotTesting
21
+ # Default snapshot directory
22
+ SNAPSHOT_DIR = "spec/snapshots/synthra"
23
+
24
+ class << self
25
+ # Snapshot directory path
26
+ attr_accessor :snapshot_dir
27
+ end
28
+
29
+ self.snapshot_dir = SNAPSHOT_DIR
30
+
31
+ # Generate and compare snapshot for a schema
32
+ #
33
+ # @param schema [Schema] the schema to snapshot
34
+ # @param seed [Integer] seed for deterministic generation
35
+ # @param update [Boolean] if true, update the snapshot file
36
+ # @return [Boolean] true if snapshot matches
37
+ #
38
+ def expect_snapshot(schema, seed: 42, update: false)
39
+ snapshot_path = snapshot_path_for(schema)
40
+ generated = schema.generate(seed: seed)
41
+ generated_json = JSON.pretty_generate(sort_hash(generated))
42
+
43
+ if update || !File.exist?(snapshot_path)
44
+ write_snapshot(snapshot_path, generated_json)
45
+ return true
46
+ end
47
+
48
+ expected = File.read(snapshot_path)
49
+
50
+ if generated_json == expected
51
+ true
52
+ else
53
+ report_snapshot_mismatch(schema.name, expected, generated_json)
54
+ false
55
+ end
56
+ end
57
+
58
+ # RSpec matcher for snapshot testing
59
+ #
60
+ # @param schema_name [String] schema name for snapshot file
61
+ # @return [SnapshotMatcher]
62
+ #
63
+ def match_snapshot(schema_name)
64
+ SnapshotMatcher.new(schema_name, snapshot_dir: SnapshotTesting.snapshot_dir)
65
+ end
66
+
67
+ # Generate snapshots for all schemas
68
+ #
69
+ # @param registry [Registry] schema registry
70
+ # @param seed [Integer] seed for deterministic generation
71
+ #
72
+ def self.generate_all_snapshots(registry, seed: 42)
73
+ FileUtils.mkdir_p(snapshot_dir)
74
+
75
+ registry.schemas.each do |name, schema|
76
+ path = File.join(snapshot_dir, "#{name.downcase}.json")
77
+ generated = schema.generate(seed: seed)
78
+ File.write(path, JSON.pretty_generate(deep_sort(generated)))
79
+ puts "Generated snapshot: #{path}"
80
+ end
81
+ end
82
+
83
+ # Verify all snapshots match
84
+ #
85
+ # @param registry [Registry] schema registry
86
+ # @param seed [Integer] seed for deterministic generation
87
+ # @return [Array<String>] list of mismatched schema names
88
+ #
89
+ def self.verify_all_snapshots(registry, seed: 42)
90
+ mismatches = []
91
+
92
+ registry.schemas.each do |name, schema|
93
+ path = File.join(snapshot_dir, "#{name.downcase}.json")
94
+
95
+ unless File.exist?(path)
96
+ mismatches << "#{name}: snapshot not found"
97
+ next
98
+ end
99
+
100
+ generated = schema.generate(seed: seed)
101
+ expected = JSON.parse(File.read(path))
102
+
103
+ unless deep_sort(generated) == deep_sort(expected)
104
+ mismatches << name
105
+ end
106
+ end
107
+
108
+ mismatches
109
+ end
110
+
111
+ # Deep sort helper for module methods
112
+ def self.deep_sort(obj)
113
+ case obj
114
+ when Hash
115
+ obj.sort.to_h.transform_values { |v| deep_sort(v) }
116
+ when Array
117
+ obj.map { |v| deep_sort(v) }
118
+ else
119
+ obj
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def snapshot_path_for(schema)
126
+ dir = SnapshotTesting.snapshot_dir
127
+ FileUtils.mkdir_p(dir)
128
+ File.join(dir, "#{schema.name.downcase}.json")
129
+ end
130
+
131
+ def write_snapshot(path, content)
132
+ FileUtils.mkdir_p(File.dirname(path))
133
+ File.write(path, content)
134
+ end
135
+
136
+ def sort_hash(obj)
137
+ case obj
138
+ when Hash
139
+ obj.sort.to_h.transform_values { |v| sort_hash(v) }
140
+ when Array
141
+ obj.map { |v| sort_hash(v) }
142
+ else
143
+ obj
144
+ end
145
+ end
146
+
147
+ def report_snapshot_mismatch(name, expected, actual)
148
+ puts "\nSnapshot mismatch for #{name}:"
149
+ puts "Expected:"
150
+ puts expected.lines.first(10).join
151
+ puts "..."
152
+ puts "\nActual:"
153
+ puts actual.lines.first(10).join
154
+ puts "..."
155
+ end
156
+
157
+ # RSpec matcher class
158
+ class SnapshotMatcher
159
+ def initialize(schema_name, snapshot_dir:)
160
+ @schema_name = schema_name
161
+ @snapshot_dir = snapshot_dir
162
+ end
163
+
164
+ def matches?(actual)
165
+ @actual = actual
166
+ path = File.join(@snapshot_dir, "#{@schema_name.downcase}.json")
167
+
168
+ return false unless File.exist?(path)
169
+
170
+ @expected = JSON.parse(File.read(path))
171
+ sorted_actual = sort_hash(@actual)
172
+ sorted_expected = sort_hash(@expected)
173
+
174
+ sorted_actual == sorted_expected
175
+ end
176
+
177
+ def failure_message
178
+ "expected generated data to match snapshot for #{@schema_name}"
179
+ end
180
+
181
+ def failure_message_when_negated
182
+ "expected generated data not to match snapshot for #{@schema_name}"
183
+ end
184
+
185
+ private
186
+
187
+ def sort_hash(obj)
188
+ case obj
189
+ when Hash
190
+ obj.sort.to_h.transform_values { |v| sort_hash(v) }
191
+ when Array
192
+ obj.map { |v| sort_hash(v) }
193
+ else
194
+ obj
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end