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,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ module Export
5
+ # GraphQL Federation Export
6
+ #
7
+ # Exports schemas to Apollo Federation v2 compatible GraphQL SDL.
8
+ # Includes @key directives, entity references, and subgraph support.
9
+ #
10
+ # @example Export federated schema
11
+ # exporter = Synthra::Export::GraphQLFederation.new(registry)
12
+ # sdl = exporter.export
13
+ #
14
+ # @example CLI
15
+ # $ synthra export --all -f graphql-federation -o schema.graphql
16
+ #
17
+ class GraphQLFederation
18
+ # Type mapping from Synthra to GraphQL
19
+ TYPE_MAP = {
20
+ "text" => "String",
21
+ "name" => "String",
22
+ "email" => "String",
23
+ "url" => "String",
24
+ "uuid" => "ID",
25
+ "id_sequence" => "ID",
26
+ "number" => "Int",
27
+ "integer" => "Int",
28
+ "age" => "Int",
29
+ "airport_elevation" => "Int",
30
+ "discount" => "Int",
31
+ "formula" => "Int",
32
+ "row_number" => "Int",
33
+ "sequence" => "Int",
34
+ "float" => "Float",
35
+ "money" => "Float",
36
+ "latitude" => "Float",
37
+ "longitude" => "Float",
38
+ "airport_latitude" => "Float",
39
+ "airport_longitude" => "Float",
40
+ "product_price" => "Float",
41
+ "tax_rate" => "Float",
42
+ "transaction_amount" => "Float",
43
+ "boolean" => "Boolean",
44
+ "timestamp" => "DateTime",
45
+ "datetime" => "DateTime",
46
+ "date" => "Date",
47
+ "object" => "JSON",
48
+ "map_by_field" => "JSON",
49
+ "array" => "[String]"
50
+ }.freeze
51
+
52
+ attr_reader :registry, :options
53
+
54
+ # Create new exporter
55
+ #
56
+ # @param registry [Registry] schema registry
57
+ # @param options [Hash] export options
58
+ # @option options [String] :service_name subgraph service name
59
+ # @option options [String] :service_url subgraph URL
60
+ # @option options [Array<String>] :key_fields default key fields
61
+ # @option options [Boolean] :include_mutations include mutation types
62
+ #
63
+ def initialize(registry, **options)
64
+ @registry = registry
65
+ @options = {
66
+ service_name: "fake-data-service",
67
+ service_url: "http://localhost:4000/graphql",
68
+ key_fields: %w[id uuid],
69
+ include_mutations: true,
70
+ include_subscriptions: false
71
+ }.merge(options)
72
+ end
73
+
74
+ # Export to Federation SDL
75
+ #
76
+ # @return [String] GraphQL SDL
77
+ #
78
+ def export
79
+ lines = []
80
+
81
+ # Federation schema extension
82
+ lines << federation_header
83
+ lines << ""
84
+
85
+ # Scalar definitions
86
+ lines << scalar_definitions
87
+ lines << ""
88
+
89
+ # Type definitions
90
+ @registry.names.sort.each do |name|
91
+ schema = @registry.schema(name)
92
+ lines << generate_type(schema)
93
+ lines << ""
94
+ end
95
+
96
+ # Query type
97
+ lines << generate_query_type
98
+ lines << ""
99
+
100
+ # Mutation type
101
+ if @options[:include_mutations]
102
+ lines << generate_mutation_type
103
+ lines << ""
104
+ end
105
+
106
+ # Subscription type
107
+ if @options[:include_subscriptions]
108
+ lines << generate_subscription_type
109
+ lines << ""
110
+ end
111
+
112
+ # Input types
113
+ @registry.names.sort.each do |name|
114
+ schema = @registry.schema(name)
115
+ lines << generate_input_type(schema)
116
+ lines << ""
117
+ end
118
+
119
+ lines.join("\n").strip
120
+ end
121
+
122
+ private
123
+
124
+ def federation_header
125
+ <<~HEADER
126
+ extend schema
127
+ @link(url: "https://specs.apollo.dev/federation/v2.0",
128
+ import: ["@key", "@shareable", "@external", "@provides", "@requires"])
129
+ HEADER
130
+ end
131
+
132
+ def scalar_definitions
133
+ <<~SCALARS
134
+ scalar DateTime
135
+ scalar Date
136
+ scalar JSON
137
+ SCALARS
138
+ end
139
+
140
+ def generate_type(schema)
141
+ lines = []
142
+
143
+ # Type declaration with @key directive
144
+ key_field = find_key_field(schema)
145
+ key_directive = key_field ? %( @key(fields: "#{key_field}")) : ""
146
+
147
+ lines << "type #{schema.name}#{key_directive} {"
148
+
149
+ # Fields
150
+ schema.fields.each do |field|
151
+ graphql_type = field_to_graphql(field)
152
+ nullable = field.optional? || field.nullable? ? "" : "!"
153
+ directives = field_directives(field, schema)
154
+
155
+ lines << " #{field.name}: #{graphql_type}#{nullable}#{directives}"
156
+ end
157
+
158
+ # Add relationship fields
159
+ add_relationship_fields(lines, schema)
160
+
161
+ lines << "}"
162
+ lines.join("\n")
163
+ end
164
+
165
+ def generate_input_type(schema)
166
+ lines = []
167
+ lines << "input #{schema.name}Input {"
168
+
169
+ schema.fields.each do |field|
170
+ next if %w[id uuid created_at updated_at].include?(field.name)
171
+
172
+ graphql_type = field_to_graphql(field, input: true)
173
+ lines << " #{field.name}: #{graphql_type}"
174
+ end
175
+
176
+ lines << "}"
177
+ lines.join("\n")
178
+ end
179
+
180
+ def generate_query_type
181
+ lines = []
182
+ lines << "type Query {"
183
+
184
+ @registry.names.sort.each do |name|
185
+ snake_name = to_snake_case(name)
186
+
187
+ # Single record by ID
188
+ lines << " #{snake_name}(id: ID!): #{name}"
189
+
190
+ # List with pagination
191
+ lines << " #{snake_name}s(first: Int, after: String, filter: #{name}Input): #{name}Connection!"
192
+
193
+ # Generate fake data
194
+ lines << " generate#{name}(seed: Int, mode: GenerationMode): #{name}!"
195
+ lines << " generate#{name}s(count: Int!, seed: Int, mode: GenerationMode): [#{name}!]!"
196
+ end
197
+
198
+ # Schema introspection
199
+ lines << ""
200
+ lines << " # Schema information"
201
+ lines << " _schemas: [SchemaInfo!]!"
202
+ lines << " _schema(name: String!): SchemaInfo"
203
+
204
+ lines << "}"
205
+
206
+ # Add supporting types
207
+ lines << ""
208
+ lines << generate_connection_types
209
+ lines << ""
210
+ lines << generate_schema_info_type
211
+ lines << ""
212
+ lines << generate_generation_mode_enum
213
+
214
+ lines.join("\n")
215
+ end
216
+
217
+ def generate_mutation_type
218
+ lines = []
219
+ lines << "type Mutation {"
220
+
221
+ @registry.names.sort.each do |name|
222
+ snake_name = to_snake_case(name)
223
+
224
+ lines << " # Generate and optionally persist #{name}"
225
+ lines << " create#{name}(input: #{name}Input, seed: Int): #{name}!"
226
+ lines << " createMany#{name}s(count: Int!, input: #{name}Input, seed: Int): [#{name}!]!"
227
+ end
228
+
229
+ lines << "}"
230
+ lines.join("\n")
231
+ end
232
+
233
+ def generate_subscription_type
234
+ lines = []
235
+ lines << "type Subscription {"
236
+
237
+ @registry.names.sort.each do |name|
238
+ snake_name = to_snake_case(name)
239
+ lines << " #{snake_name}Generated: #{name}!"
240
+ end
241
+
242
+ lines << "}"
243
+ lines.join("\n")
244
+ end
245
+
246
+ def generate_connection_types
247
+ types = @registry.names.map do |name|
248
+ <<~CONNECTION
249
+ type #{name}Connection {
250
+ edges: [#{name}Edge!]!
251
+ pageInfo: PageInfo!
252
+ totalCount: Int!
253
+ }
254
+
255
+ type #{name}Edge {
256
+ cursor: String!
257
+ node: #{name}!
258
+ }
259
+ CONNECTION
260
+ end
261
+
262
+ page_info = <<~PAGEINFO
263
+ type PageInfo {
264
+ hasNextPage: Boolean!
265
+ hasPreviousPage: Boolean!
266
+ startCursor: String
267
+ endCursor: String
268
+ }
269
+ PAGEINFO
270
+
271
+ [page_info, *types].join("\n")
272
+ end
273
+
274
+ def generate_schema_info_type
275
+ <<~SCHEMA_INFO
276
+ type SchemaInfo {
277
+ name: String!
278
+ fields: [FieldInfo!]!
279
+ behaviors: [String!]!
280
+ }
281
+
282
+ type FieldInfo {
283
+ name: String!
284
+ type: String!
285
+ optional: Boolean!
286
+ nullable: Boolean!
287
+ }
288
+ SCHEMA_INFO
289
+ end
290
+
291
+ def generate_generation_mode_enum
292
+ <<~ENUM
293
+ enum GenerationMode {
294
+ RANDOM
295
+ EDGE
296
+ INVALID
297
+ HOSTILE
298
+ MIXED
299
+ }
300
+ ENUM
301
+ end
302
+
303
+ def field_to_graphql(field, input: false)
304
+ type_name = field.type_name
305
+
306
+ # Handle arrays
307
+ if type_name == "array"
308
+ element = field.type_args[:element]
309
+ inner_type = if @registry.schema?(element.to_s)
310
+ input ? "#{element}Input" : element.to_s
311
+ else
312
+ TYPE_MAP[element.to_s] || "String"
313
+ end
314
+ return "[#{inner_type}!]"
315
+ end
316
+
317
+ # Handle enums
318
+ if type_name == "enum"
319
+ return "#{field.name.capitalize}Enum"
320
+ end
321
+
322
+ # Handle references
323
+ if @registry.schema?(type_name)
324
+ return input ? "#{type_name}Input" : type_name
325
+ end
326
+
327
+ TYPE_MAP[type_name] || "String"
328
+ end
329
+
330
+ def field_directives(field, schema)
331
+ directives = []
332
+
333
+ # Mark shareable fields
334
+ if %w[id uuid].include?(field.name)
335
+ # Key fields are automatically shareable
336
+ elsif field.type_name == "timestamp"
337
+ directives << "@shareable"
338
+ end
339
+
340
+ # External references
341
+ if field.type_name.start_with?("Ref(")
342
+ directives << "@external"
343
+ end
344
+
345
+ directives.empty? ? "" : " #{directives.join(' ')}"
346
+ end
347
+
348
+ def add_relationship_fields(lines, schema)
349
+ # Check for relationship indicators in field names
350
+ schema.fields.each do |field|
351
+ if field.name.end_with?("_id") && !field.name.start_with?("external_")
352
+ # Infer relationship
353
+ related_name = field.name.sub(/_id$/, "").split("_").map(&:capitalize).join
354
+ if @registry.schema?(related_name)
355
+ lines << ""
356
+ lines << " # Resolved relationship"
357
+ lines << " #{to_snake_case(related_name)}: #{related_name} @provides(fields: \"id\")"
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ def find_key_field(schema)
364
+ @options[:key_fields].each do |key|
365
+ return key if schema.fields.any? { |f| f.name == key }
366
+ end
367
+ nil
368
+ end
369
+
370
+ def to_snake_case(str)
371
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
372
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
373
+ .downcase
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Synthra
5
+ module Export
6
+ # Exports schemas as GraphViz DOT format
7
+ #
8
+ # @example Export a registry as DOT
9
+ # exporter = Graphviz.new(registry: registry)
10
+ # dot = exporter.export
11
+ #
12
+ class Graphviz < Base
13
+ # Color schemes for different schema types
14
+ COLORS = {
15
+ default: "black",
16
+ deprecated: "gray",
17
+ main: "#2563eb",
18
+ nested: "#059669"
19
+ }.freeze
20
+
21
+ # Edge styles for different relationship types
22
+ EDGE_STYLES = {
23
+ embed: "",
24
+ array: "style=dashed",
25
+ map_by_field: "style=dotted",
26
+ reference: "style=bold, color=\"#dc2626\""
27
+ }.freeze
28
+
29
+ def initialize(schema = nil, registry: nil, **options)
30
+ super
31
+ @registry = registry
32
+ end
33
+
34
+ def export
35
+ lines = []
36
+ lines << "digraph Synthra {"
37
+ lines << " // Graph settings"
38
+ lines << " rankdir=#{options[:direction] || 'LR'};"
39
+ lines << " splines=ortho;" if options[:ortho]
40
+ lines << " node [shape=record, fontname=\"#{options[:font] || 'Helvetica'}\", fontsize=10];"
41
+ lines << " edge [fontname=\"#{options[:font] || 'Helvetica'}\", fontsize=9];"
42
+ lines << ""
43
+
44
+ if @registry
45
+ lines << build_registry_graph
46
+ elsif @schema
47
+ lines << build_schema_node(@schema)
48
+ end
49
+
50
+ lines << "}"
51
+ lines.join("\n")
52
+ end
53
+
54
+ # Render DOT to image format using graphviz
55
+ #
56
+ # @param output_path [String] output file path
57
+ # @param format [Symbol] output format (:png, :svg, :pdf)
58
+ # @return [Boolean] true if successful
59
+ #
60
+ def render(output_path, format: :png)
61
+ dot_content = export
62
+
63
+ require "tempfile"
64
+ Tempfile.create(["schema", ".dot"]) do |dot_file|
65
+ dot_file.write(dot_content)
66
+ dot_file.flush
67
+
68
+ system("dot", "-T#{format}", dot_file.path, "-o", output_path)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def build_registry_graph
75
+ lines = []
76
+
77
+ # Group schemas by category if specified
78
+ if options[:group]
79
+ lines.concat(build_grouped_nodes)
80
+ else
81
+ @registry.schemas.each do |name, schema|
82
+ lines << build_schema_node(schema)
83
+ end
84
+ end
85
+
86
+ lines << ""
87
+
88
+ # Add edges
89
+ @registry.schemas.each do |name, schema|
90
+ lines.concat(build_schema_edges(schema))
91
+ end
92
+
93
+ lines
94
+ end
95
+
96
+ def build_grouped_nodes
97
+ lines = []
98
+ groups = Hash.new { |h, k| h[k] = [] }
99
+
100
+ @registry.schemas.each do |name, schema|
101
+ group = detect_group(name)
102
+ groups[group] << schema
103
+ end
104
+
105
+ groups.each do |group, schemas|
106
+ lines << " subgraph cluster_#{group} {"
107
+ lines << " label=\"#{group.capitalize}\";"
108
+ lines << " style=dashed;"
109
+ schemas.each { |s| lines << build_schema_node(s, indent: 4) }
110
+ lines << " }"
111
+ end
112
+
113
+ lines
114
+ end
115
+
116
+ def detect_group(name)
117
+ case name
118
+ when /User|Profile|Account/i then "users"
119
+ when /Order|Cart|Payment/i then "commerce"
120
+ when /Message|Chat|DM/i then "messaging"
121
+ when /API|Response|Request/i then "api"
122
+ else "other"
123
+ end
124
+ end
125
+
126
+ def build_schema_node(target_schema, indent: 2)
127
+ prefix = " " * indent
128
+ name = target_schema.name
129
+
130
+ fields = target_schema.fields.map do |f|
131
+ optional = f.optional? ? "?" : ""
132
+ nullable = f.nullable? ? "?" : ""
133
+ type_info = format_type(f)
134
+ "#{f.name}#{optional}: #{type_info}#{nullable}"
135
+ end
136
+
137
+ # Escape special characters for DOT
138
+ escaped_fields = fields.map { |f| escape_dot(f) }.join("\\l")
139
+ label = "{#{name}|#{escaped_fields}\\l}"
140
+
141
+ color = target_schema.deprecated? ? COLORS[:deprecated] : COLORS[:default]
142
+ style = target_schema.deprecated? ? ", style=dashed" : ""
143
+
144
+ "#{prefix}#{name} [label=\"#{label}\", color=\"#{color}\"#{style}];"
145
+ end
146
+
147
+ def build_schema_edges(source_schema)
148
+ lines = []
149
+
150
+ source_schema.fields.each do |field|
151
+ edges = field_to_edges(source_schema.name, field)
152
+ lines.concat(edges)
153
+ end
154
+
155
+ lines
156
+ end
157
+
158
+ def field_to_edges(source_name, field)
159
+ edges = []
160
+ type_name = field.type_name
161
+ type_args = field.type_args
162
+
163
+ # Direct schema reference
164
+ if schema_in_registry?(type_name)
165
+ edges << build_edge(source_name, type_name, field.name, :embed)
166
+ end
167
+
168
+ # Array element type
169
+ if type_name == "array"
170
+ element = type_args[:element]
171
+ if element && schema_in_registry?(element.to_s)
172
+ edges << build_edge(source_name, element.to_s, "#{field.name}[]", :array)
173
+ end
174
+ end
175
+
176
+ # map_by_field entries
177
+ if type_name == "map_by_field"
178
+ entries = type_args[:entries] || {}
179
+ entries.each do |entry_name, entry_info|
180
+ schema_name = entry_info.is_a?(Hash) ? (entry_info[:schema] || entry_info["schema"]) : entry_info.to_s
181
+ if schema_name && schema_in_registry?(schema_name)
182
+ shared = entry_info.is_a?(Hash) && entry_info[:shared]
183
+ label = shared ? "#{field.name}.#{entry_name} (shared)" : "#{field.name}.#{entry_name}"
184
+ edges << build_edge(source_name, schema_name, label, :map_by_field)
185
+ end
186
+ end
187
+ end
188
+
189
+ # one_of schemas
190
+ if type_name == "one_of"
191
+ schemas = type_args[:schemas] || []
192
+ schemas.each do |s|
193
+ schema_name = s.is_a?(Hash) ? (s[:schema] || s["schema"]) : s.to_s
194
+ if schema_name && schema_in_registry?(schema_name)
195
+ edges << build_edge(source_name, schema_name, "#{field.name}|", :reference)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Ref() references
201
+ if type_name == "reference"
202
+ ref = type_args[:ref]
203
+ if ref.respond_to?(:schema_name) && schema_in_registry?(ref.schema_name)
204
+ edges << build_edge(source_name, ref.schema_name, "Ref(#{ref.schema_name}.#{ref.field_path})", :reference)
205
+ end
206
+ end
207
+
208
+ edges
209
+ end
210
+
211
+ def build_edge(source, target, label, type)
212
+ style = EDGE_STYLES[type] || ""
213
+ style_attr = style.empty? ? "" : ", #{style}"
214
+ " #{source} -> #{target} [label=\"#{escape_dot(label)}\"#{style_attr}];"
215
+ end
216
+
217
+ def format_type(field)
218
+ type_name = field.type_name
219
+ type_args = field.type_args
220
+
221
+ case type_name
222
+ when "array"
223
+ element = type_args[:element] || "?"
224
+ "#{element}[]"
225
+ when "enum"
226
+ values = type_args[:values]&.map { |v| v.respond_to?(:value) ? v.value : v } || []
227
+ values.length <= 3 ? values.join("|") : "enum(#{values.length})"
228
+ when "const"
229
+ "\"#{type_args[:value]}\""
230
+ when "one_of"
231
+ schemas = type_args[:schemas] || []
232
+ names = schemas.map { |s| s.is_a?(Hash) ? s[:schema] : s.to_s }
233
+ names.length <= 2 ? names.join("|") : "one_of(#{names.length})"
234
+ else
235
+ type_name
236
+ end
237
+ end
238
+
239
+ def schema_in_registry?(name)
240
+ @registry&.schema?(name) || false
241
+ end
242
+
243
+ def escape_dot(str)
244
+ str.to_s
245
+ .gsub("\\", "\\\\")
246
+ .gsub("\"", "\\\"")
247
+ .gsub("{", "\\{")
248
+ .gsub("}", "\\}")
249
+ .gsub("<", "\\<")
250
+ .gsub(">", "\\>")
251
+ .gsub("|", "\\|")
252
+ end
253
+ end
254
+ # :nocov:
255
+ end
256
+ end
257
+
258
+