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,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Schema Migration Generator
5
+ #
6
+ # Generate database migrations from schema changes.
7
+ # Works with the ContractsRegistry to track versions
8
+ # and produce Rails-compatible migrations.
9
+ #
10
+ # @example Generate migration from diff
11
+ # MigrationGenerator.generate(
12
+ # from: "User@v1.0.0",
13
+ # to: "User@v2.0.0",
14
+ # output: "db/migrate/"
15
+ # )
16
+ #
17
+ # @example CLI
18
+ # $ synthra schema migrate User --from v1.0.0 --to v2.0.0
19
+ #
20
+ class MigrationGenerator
21
+ # Type mapping from Synthra to ActiveRecord
22
+ TYPE_MAP = {
23
+ "text" => "string",
24
+ "name" => "string",
25
+ "email" => "string",
26
+ "url" => "string",
27
+ "paragraph" => "text",
28
+ "uuid" => "uuid",
29
+ "number" => "integer",
30
+ "integer" => "integer",
31
+ "float" => "float",
32
+ "money" => "decimal, precision: 10, scale: 2",
33
+ "decimal" => "decimal, precision: 10, scale: 2",
34
+ "boolean" => "boolean",
35
+ "timestamp" => "datetime",
36
+ "datetime" => "datetime",
37
+ "date" => "date",
38
+ "time" => "time",
39
+ "object" => "jsonb",
40
+ "json" => "jsonb",
41
+ "binary" => "binary",
42
+ "array" => "text, array: true"
43
+ }.freeze
44
+
45
+ class << self
46
+ private
47
+
48
+ # Convert CamelCase to snake_case
49
+ def underscore(str)
50
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
51
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
52
+ .downcase
53
+ end
54
+
55
+ # Simple pluralization
56
+ def pluralize(str)
57
+ if str.end_with?('s', 'x', 'z', 'ch', 'sh')
58
+ "#{str}es"
59
+ elsif str.end_with?('y') && !%w[a e i o u].include?(str[-2])
60
+ "#{str[0..-2]}ies"
61
+ else
62
+ "#{str}s"
63
+ end
64
+ end
65
+
66
+ public
67
+
68
+ # Generate a migration from schema changes
69
+ #
70
+ # @param from [String] source schema/version
71
+ # @param to [String] target schema/version
72
+ # @param output [String] output directory
73
+ # @param options [Hash] generation options
74
+ # @return [String] migration file path
75
+ #
76
+ def generate(from:, to:, output: nil, **options)
77
+ from_schema = resolve_schema(from)
78
+ to_schema = resolve_schema(to)
79
+
80
+ diff = calculate_diff(from_schema, to_schema)
81
+ migration = build_migration(from_schema, to_schema, diff, options)
82
+
83
+ if output
84
+ write_migration(migration, output, from_schema, to_schema)
85
+ else
86
+ migration
87
+ end
88
+ end
89
+
90
+ # Generate migration from two schema files
91
+ #
92
+ # @param old_path [String] path to old schema
93
+ # @param new_path [String] path to new schema
94
+ # @param output [String] output directory
95
+ # @return [String] migration content or file path
96
+ #
97
+ def from_files(old_path:, new_path:, output: nil)
98
+ old_reg = Synthra::Registry.new
99
+ old_reg.load_file(old_path)
100
+ old_schemas = old_reg.schemas.values
101
+
102
+ new_reg = Synthra::Registry.new
103
+ new_reg.load_file(new_path)
104
+ new_schemas = new_reg.schemas.values
105
+
106
+ # Match by name
107
+ old_schema = old_schemas.first
108
+ new_schema = new_schemas.first
109
+
110
+ diff = calculate_diff(old_schema, new_schema)
111
+ migration = build_migration(old_schema, new_schema, diff, {})
112
+
113
+ if output
114
+ write_migration(migration, output, old_schema, new_schema)
115
+ else
116
+ migration
117
+ end
118
+ end
119
+
120
+ # Generate initial migration for a schema
121
+ #
122
+ # @param schema [Schema, String] schema to migrate
123
+ # @param output [String] output directory
124
+ # @return [String] migration content or file path
125
+ #
126
+ def create_table(schema, output: nil)
127
+ schema = resolve_schema(schema) if schema.is_a?(String)
128
+
129
+ migration = build_create_table_migration(schema)
130
+
131
+ if output
132
+ write_migration(migration, output, nil, schema, action: "create")
133
+ else
134
+ migration
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ def resolve_schema(identifier)
141
+ # Handle versioned identifiers like "User@v1.0.0"
142
+ if identifier.include?("@")
143
+ name, version = identifier.split("@")
144
+ contracts = Synthra::ContractsRegistry.new("contracts")
145
+ contract = contracts.get(name, version: version)
146
+ raise ArgumentError, "Contract not found: #{identifier}" unless contract
147
+
148
+ # Rebuild schema from contract
149
+ reg = Synthra::Registry.new
150
+ reg.load_string(contract[:dsl_content])
151
+ reg.schemas.values.first
152
+ elsif Synthra.registry.schema?(identifier)
153
+ Synthra.registry.schema(identifier)
154
+ else
155
+ raise ArgumentError, "Schema not found: #{identifier}"
156
+ end
157
+ end
158
+
159
+ def calculate_diff(old_schema, new_schema)
160
+ old_fields = old_schema.fields.each_with_object({}) { |f, h| h[f.name] = f }
161
+ new_fields = new_schema.fields.each_with_object({}) { |f, h| h[f.name] = f }
162
+
163
+ {
164
+ added: new_fields.keys - old_fields.keys,
165
+ removed: old_fields.keys - new_fields.keys,
166
+ type_changed: (old_fields.keys & new_fields.keys).select do |name|
167
+ old_fields[name].type_name != new_fields[name].type_name
168
+ end,
169
+ nullable_changed: (old_fields.keys & new_fields.keys).select do |name|
170
+ old_fields[name].nullable? != new_fields[name].nullable? ||
171
+ old_fields[name].optional? != new_fields[name].optional?
172
+ end,
173
+ old_fields: old_fields,
174
+ new_fields: new_fields
175
+ }
176
+ end
177
+
178
+ def build_migration(old_schema, new_schema, diff, options)
179
+ table_name = pluralize(underscore(new_schema.name))
180
+ class_name = "Migrate#{old_schema.name}To#{new_schema.name.sub(/.*/, '')}"
181
+
182
+ # If versions exist in names
183
+ if old_schema.name.include?("@") || new_schema.name.include?("@")
184
+ old_version = old_schema.name.split("@").last rescue "v1"
185
+ new_version = new_schema.name.split("@").last rescue "v2"
186
+ class_name = "Migrate#{old_schema.name.split('@').first}#{old_version.gsub('.', '')}To#{new_version.gsub('.', '')}"
187
+ else
188
+ class_name = "Update#{new_schema.name}Schema"
189
+ end
190
+
191
+ changes = []
192
+
193
+ # Added columns
194
+ diff[:added].each do |name|
195
+ field = diff[:new_fields][name]
196
+ ar_type = TYPE_MAP[field.type_name] || "string"
197
+ null = field.optional? || field.nullable? ? "null: true" : "null: false"
198
+ changes << " add_column :#{table_name}, :#{name}, :#{ar_type}, #{null}"
199
+ end
200
+
201
+ # Removed columns
202
+ diff[:removed].each do |name|
203
+ field = diff[:old_fields][name]
204
+ ar_type = TYPE_MAP[field.type_name] || "string"
205
+ changes << " remove_column :#{table_name}, :#{name}, :#{ar_type}"
206
+ end
207
+
208
+ # Type changes
209
+ diff[:type_changed].each do |name|
210
+ old_field = diff[:old_fields][name]
211
+ new_field = diff[:new_fields][name]
212
+ new_type = TYPE_MAP[new_field.type_name] || "string"
213
+ changes << " change_column :#{table_name}, :#{name}, :#{new_type} # was: #{old_field.type_name}"
214
+ end
215
+
216
+ # Nullable changes
217
+ diff[:nullable_changed].each do |name|
218
+ next if diff[:type_changed].include?(name)
219
+
220
+ new_field = diff[:new_fields][name]
221
+ null = new_field.optional? || new_field.nullable?
222
+ changes << " change_column_null :#{table_name}, :#{name}, #{null}"
223
+ end
224
+
225
+ <<~MIGRATION
226
+ # frozen_string_literal: true
227
+
228
+ # Generated by Synthra
229
+ # From: #{old_schema.name}
230
+ # To: #{new_schema.name}
231
+
232
+ class #{class_name} < ActiveRecord::Migration[7.1]
233
+ def change
234
+ #{changes.join("\n")}
235
+ end
236
+ end
237
+ MIGRATION
238
+ end
239
+
240
+ def build_create_table_migration(schema)
241
+ table_name = pluralize(underscore(schema.name))
242
+ class_name = "Create#{pluralize(schema.name)}"
243
+
244
+ columns = schema.fields.map do |field|
245
+ ar_type = TYPE_MAP[field.type_name] || "string"
246
+ null = field.optional? || field.nullable? ? "null: true" : "null: false"
247
+
248
+ if field.name == "id"
249
+ nil # Skip id, handled by primary key
250
+ else
251
+ " t.#{ar_type.split(',').first} :#{field.name}, #{null}"
252
+ end
253
+ end.compact
254
+
255
+ # Check for timestamps
256
+ has_created_at = schema.fields.any? { |f| f.name == "created_at" }
257
+ has_updated_at = schema.fields.any? { |f| f.name == "updated_at" }
258
+
259
+ timestamps = if has_created_at && has_updated_at
260
+ columns.reject! { |c| c.include?(":created_at") || c.include?(":updated_at") }
261
+ "\n t.timestamps"
262
+ else
263
+ ""
264
+ end
265
+
266
+ <<~MIGRATION
267
+ # frozen_string_literal: true
268
+
269
+ # Generated by Synthra
270
+ # Schema: #{schema.name}
271
+
272
+ class #{class_name} < ActiveRecord::Migration[7.1]
273
+ def change
274
+ create_table :#{table_name}, id: :uuid do |t|
275
+ #{columns.join("\n")}#{timestamps}
276
+ end
277
+ end
278
+ end
279
+ MIGRATION
280
+ end
281
+
282
+ def write_migration(content, output_dir, old_schema, new_schema, action: "update")
283
+ FileUtils.mkdir_p(output_dir)
284
+
285
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
286
+ schema_name = underscore(new_schema.name)
287
+
288
+ filename = case action
289
+ when "create"
290
+ "#{timestamp}_create_#{pluralize(schema_name)}.rb"
291
+ else
292
+ "#{timestamp}_update_#{schema_name}.rb"
293
+ end
294
+
295
+ path = File.join(output_dir, filename)
296
+ File.write(path, content)
297
+ path
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Schema Mixin Support
5
+ #
6
+ # Allows defining reusable field sets that can be included in multiple schemas.
7
+ # More flexible than inheritance for cross-cutting concerns.
8
+ #
9
+ # @example Define a mixin
10
+ # @mixin Timestamps:
11
+ # created_at: timestamp
12
+ # updated_at: timestamp
13
+ #
14
+ # @example Use in a schema
15
+ # User:
16
+ # @include Timestamps
17
+ # id: uuid
18
+ # name: name
19
+ #
20
+ module Mixin
21
+ # Registry for storing mixins
22
+ class Registry
23
+ class << self
24
+ def instance
25
+ @instance ||= new
26
+ end
27
+
28
+ def register(name, fields:, behaviors: [])
29
+ instance.register(name, fields: fields, behaviors: behaviors)
30
+ end
31
+
32
+ def get(name)
33
+ instance.get(name)
34
+ end
35
+
36
+ def exists?(name)
37
+ instance.exists?(name)
38
+ end
39
+
40
+ def clear
41
+ instance.clear
42
+ end
43
+
44
+ def all
45
+ instance.all
46
+ end
47
+ end
48
+
49
+ def initialize
50
+ @mixins = {}
51
+ @mutex = Mutex.new
52
+ end
53
+
54
+ # Register a mixin
55
+ #
56
+ # @param name [String] mixin name
57
+ # @param fields [Array<Field>] mixin fields
58
+ # @param behaviors [Array<Hash>] mixin behaviors
59
+ #
60
+ def register(name, fields:, behaviors: [])
61
+ @mutex.synchronize do
62
+ @mixins[name.to_s] = {
63
+ name: name.to_s,
64
+ fields: fields,
65
+ behaviors: behaviors
66
+ }
67
+ end
68
+ end
69
+
70
+ # Get a mixin by name
71
+ #
72
+ # @param name [String] mixin name
73
+ # @return [Hash, nil] mixin definition
74
+ #
75
+ def get(name)
76
+ @mutex.synchronize { @mixins[name.to_s] }
77
+ end
78
+
79
+ # Check if mixin exists
80
+ #
81
+ # @param name [String] mixin name
82
+ # @return [Boolean]
83
+ #
84
+ def exists?(name)
85
+ @mutex.synchronize { @mixins.key?(name.to_s) }
86
+ end
87
+
88
+ # Clear all mixins
89
+ def clear
90
+ @mutex.synchronize { @mixins.clear }
91
+ end
92
+
93
+ # Get all mixins
94
+ def all
95
+ @mutex.synchronize { @mixins.dup }
96
+ end
97
+ end
98
+
99
+ # Apply mixins to a schema
100
+ #
101
+ # @param schema_node [AST::SchemaNode] the schema node
102
+ # @param mixin_names [Array<String>] names of mixins to include
103
+ # @return [AST::SchemaNode] schema with mixin fields added
104
+ #
105
+ def self.apply(schema_node, mixin_names)
106
+ return schema_node if mixin_names.empty?
107
+
108
+ all_fields = []
109
+ all_behaviors = schema_node.behaviors.dup
110
+
111
+ # Add fields from each mixin (in order)
112
+ mixin_names.each do |mixin_name|
113
+ mixin = Registry.get(mixin_name)
114
+ raise Errors::MixinError, "Unknown mixin: #{mixin_name}" unless mixin
115
+
116
+ all_fields.concat(mixin[:fields])
117
+ all_behaviors.concat(mixin[:behaviors])
118
+ end
119
+
120
+ # Add schema's own fields (override mixin fields if same name)
121
+ existing_names = all_fields.map { |f| f.respond_to?(:name) ? f.name : f[:name] }
122
+ schema_node.fields.each do |field|
123
+ field_name = field.respond_to?(:name) ? field.name : field[:name]
124
+ if existing_names.include?(field_name)
125
+ # Replace mixin field with schema field
126
+ all_fields.reject! { |f| (f.respond_to?(:name) ? f.name : f[:name]) == field_name }
127
+ end
128
+ all_fields << field
129
+ end
130
+
131
+ # Create new schema node with merged fields
132
+ Parser::AST::SchemaNode.new(
133
+ name: schema_node.name,
134
+ fields: all_fields,
135
+ behaviors: all_behaviors,
136
+ parent_name: schema_node.parent_name,
137
+ line: schema_node.line
138
+ )
139
+ end
140
+ end
141
+
142
+ # Common mixins that come pre-registered
143
+ module CommonMixins
144
+ def self.register_all!
145
+ # Timestamps mixin
146
+ Mixin::Registry.register("Timestamps", fields: [
147
+ Field.new(name: "created_at", type_name: "timestamp"),
148
+ Field.new(name: "updated_at", type_name: "timestamp")
149
+ ])
150
+
151
+ # Soft delete mixin
152
+ Mixin::Registry.register("SoftDelete", fields: [
153
+ Field.new(name: "deleted_at?", type_name: "timestamp?")
154
+ ])
155
+
156
+ # Auditable mixin
157
+ Mixin::Registry.register("Auditable", fields: [
158
+ Field.new(name: "created_by?", type_name: "uuid"),
159
+ Field.new(name: "updated_by?", type_name: "uuid")
160
+ ])
161
+
162
+ # Sluggable mixin
163
+ Mixin::Registry.register("Sluggable", fields: [
164
+ Field.new(name: "slug", type_name: "text")
165
+ ])
166
+
167
+ # Publishable mixin
168
+ Mixin::Registry.register("Publishable", fields: [
169
+ Field.new(name: "published_at?", type_name: "timestamp?"),
170
+ Field.new(name: "published", type_name: "boolean")
171
+ ])
172
+ end
173
+ end
174
+ end