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,560 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ module Synthra
5
+ module Export
6
+ # Exports schema as Python with Pydantic models or dataclasses
7
+ #
8
+ # @example Export as Pydantic
9
+ # exporter = Python.new(schema, registry: registry, style: :pydantic)
10
+ # python = exporter.export
11
+ #
12
+ # @example Export as dataclass
13
+ # exporter = Python.new(schema, registry: registry, style: :dataclass)
14
+ # python = exporter.export
15
+ #
16
+ class Python < Base
17
+ # Type mappings from DSL types to Python types
18
+ TYPE_MAPPINGS = {
19
+ # Strings
20
+ "uuid" => "UUID",
21
+ "ulid" => "str",
22
+ "email" => "EmailStr",
23
+ "url" => "HttpUrl",
24
+ "ip" => "IPvAnyAddress",
25
+ "ipv6" => "IPvAnyAddress",
26
+ "date" => "date",
27
+ "past_date" => "date",
28
+ "future_date" => "date",
29
+ "timestamp" => "datetime",
30
+ "text" => "str",
31
+ "name" => "str",
32
+ "full_name" => "str",
33
+ "first_name" => "str",
34
+ "last_name" => "str",
35
+ "phone" => "str",
36
+ "city" => "str",
37
+ "country" => "str",
38
+ "country_code" => "str",
39
+ "postal_code" => "str",
40
+ "state" => "str",
41
+ "street" => "str",
42
+ "address" => "str",
43
+ "username" => "str",
44
+ "social_handle" => "str",
45
+ "domain" => "str",
46
+ "user_agent" => "str",
47
+ "mac_address" => "str",
48
+ "iban" => "str",
49
+ "credit_card" => "str",
50
+ "currency" => "str",
51
+ "currency_code" => "str",
52
+ "paragraph" => "str",
53
+ "sentence" => "str",
54
+ "word" => "str",
55
+ "message_text" => "str",
56
+ "hashtag_text" => "str",
57
+ "snowflake_id" => "str",
58
+ "numeric_string_id" => "str",
59
+
60
+ # Numbers (integers)
61
+ "number" => "int",
62
+ "integer" => "int",
63
+ "age" => "int",
64
+ "airport_elevation" => "int",
65
+ "discount" => "int",
66
+ "formula" => "int",
67
+ "row_number" => "int",
68
+ "sequence" => "int",
69
+ "id_sequence" => "int",
70
+
71
+ # Numbers (floats)
72
+ "float" => "float",
73
+ "money" => "Decimal",
74
+ "latitude" => "float",
75
+ "longitude" => "float",
76
+ "airport_latitude" => "float",
77
+ "airport_longitude" => "float",
78
+ "product_price" => "float",
79
+ "tax_rate" => "float",
80
+ "transaction_amount" => "float",
81
+
82
+ # Boolean
83
+ "boolean" => "bool",
84
+
85
+ # Date (generate Date objects)
86
+ "date_between" => "date",
87
+ "mobile_device_release_date" => "date",
88
+ "now" => "date",
89
+
90
+ # Date/Time
91
+ "datetime" => "datetime",
92
+
93
+ # Objects
94
+ "object" => "Dict[str, Any]",
95
+ "map_by_field" => "Dict[str, Any]",
96
+
97
+ # Arrays
98
+ "array" => "List[Any]",
99
+ "empty_array" => "List[Any]",
100
+ "json_array" => "List[Any]",
101
+ "indices_pair" => "Tuple[int, int]"
102
+ }.freeze
103
+
104
+ # Pydantic imports needed based on types used
105
+ PYDANTIC_IMPORTS = {
106
+ "UUID" => "from uuid import UUID",
107
+ "EmailStr" => "from pydantic import EmailStr",
108
+ "HttpUrl" => "from pydantic import HttpUrl",
109
+ "IPvAnyAddress" => "from pydantic import IPvAnyAddress",
110
+ "date" => "from datetime import date",
111
+ "datetime" => "from datetime import datetime",
112
+ "Decimal" => "from decimal import Decimal"
113
+ }.freeze
114
+
115
+ def export
116
+ style = options[:style] || :pydantic
117
+
118
+ case style
119
+ when :pydantic
120
+ export_pydantic
121
+ when :dataclass
122
+ export_dataclass
123
+ when :typed_dict
124
+ export_typed_dict
125
+ else
126
+ export_pydantic # fallback
127
+ end
128
+ end
129
+
130
+ # Export all schemas in a registry
131
+ #
132
+ # @param registry [Registry] the registry
133
+ # @param options [Hash] export options
134
+ # @return [String] Python code
135
+ #
136
+ def self.export_all(registry, **options)
137
+ style = options[:style] || :pydantic
138
+
139
+ lines = []
140
+ lines << "# Generated from Synthra schemas"
141
+ lines << "# Schemas: #{registry.names.join(', ')}"
142
+ lines << ""
143
+
144
+ # Collect all types used
145
+ all_types = Set.new
146
+ registry.schemas.each do |name, schema|
147
+ schema.fields.each { |f| all_types << TYPE_MAPPINGS[f.type_name] }
148
+ end
149
+
150
+ # Add imports - use first schema to create instance for build_imports
151
+ first_schema = registry.schemas.values.first
152
+ if first_schema
153
+ exporter = new(first_schema, registry: registry, **options)
154
+ lines << exporter.send(:build_imports, all_types, style)
155
+ lines << ""
156
+ end
157
+
158
+ registry.schemas.each do |name, schema|
159
+ exporter = new(schema, registry: registry, **options)
160
+ lines << exporter.build_model(schema, style)
161
+ lines << ""
162
+ end
163
+
164
+ lines.join("\n")
165
+ end
166
+
167
+ def build_model(target_schema, style)
168
+ case style
169
+ when :pydantic
170
+ build_pydantic_model(target_schema)
171
+ when :dataclass
172
+ build_dataclass(target_schema)
173
+ when :typed_dict
174
+ build_typed_dict(target_schema)
175
+ else
176
+ build_pydantic_model(target_schema)
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def export_pydantic
183
+ lines = []
184
+
185
+ # Header
186
+ lines << '"""'
187
+ lines << "Generated from Synthra schema: #{schema.name}"
188
+ lines << "Version: #{schema.version}" if schema.version
189
+ lines << '"""'
190
+ lines << ""
191
+
192
+ # Collect types used
193
+ types_used = collect_types_used
194
+
195
+ # Imports
196
+ lines << build_imports(types_used, :pydantic)
197
+ lines << ""
198
+
199
+ # Export referenced schemas first
200
+ exported = Set.new
201
+ collect_referenced_schemas.each do |ref_schema|
202
+ lines << build_pydantic_model(ref_schema)
203
+ lines << ""
204
+ exported << ref_schema.name
205
+ end
206
+
207
+ # Export main schema
208
+ lines << build_pydantic_model(schema) unless exported.include?(schema.name)
209
+
210
+ lines.join("\n")
211
+ end
212
+
213
+ def export_dataclass
214
+ lines = []
215
+
216
+ lines << '"""'
217
+ lines << "Generated from Synthra schema: #{schema.name}"
218
+ lines << '"""'
219
+ lines << ""
220
+
221
+ types_used = collect_types_used
222
+ lines << build_imports(types_used, :dataclass)
223
+ lines << ""
224
+
225
+ exported = Set.new
226
+ collect_referenced_schemas.each do |ref_schema|
227
+ lines << build_dataclass(ref_schema)
228
+ lines << ""
229
+ exported << ref_schema.name
230
+ end
231
+
232
+ lines << build_dataclass(schema) unless exported.include?(schema.name)
233
+ lines.join("\n")
234
+ end
235
+
236
+ def export_typed_dict
237
+ lines = []
238
+
239
+ lines << '"""'
240
+ lines << "Generated from Synthra schema: #{schema.name}"
241
+ lines << '"""'
242
+ lines << ""
243
+
244
+ types_used = collect_types_used
245
+ lines << build_imports(types_used, :typed_dict)
246
+ lines << ""
247
+
248
+ exported = Set.new
249
+ collect_referenced_schemas.each do |ref_schema|
250
+ lines << build_typed_dict(ref_schema)
251
+ lines << ""
252
+ exported << ref_schema.name
253
+ end
254
+
255
+ lines << build_typed_dict(schema) unless exported.include?(schema.name)
256
+ lines.join("\n")
257
+ end
258
+
259
+ def build_pydantic_model(target_schema)
260
+ lines = []
261
+
262
+ if target_schema.deprecated?
263
+ lines << "# DEPRECATED: #{target_schema.deprecation_message || 'This model is deprecated'}"
264
+ end
265
+
266
+ lines << "class #{target_schema.name}(BaseModel):"
267
+
268
+ if target_schema.version
269
+ lines << " \"\"\"#{target_schema.name} model (v#{target_schema.version})\"\"\""
270
+ end
271
+
272
+ if target_schema.fields.empty?
273
+ lines << " pass"
274
+ else
275
+ target_schema.fields.each do |field|
276
+ lines << " #{build_pydantic_field(field)}"
277
+ end
278
+
279
+ # Add Config if needed
280
+ if options[:orm_mode] || options[:validate_assignment]
281
+ lines << ""
282
+ lines << " class Config:"
283
+ lines << " orm_mode = True" if options[:orm_mode]
284
+ lines << " validate_assignment = True" if options[:validate_assignment]
285
+ end
286
+ end
287
+
288
+ lines.join("\n")
289
+ end
290
+
291
+ def build_pydantic_field(field)
292
+ py_type = field_to_python(field)
293
+ field_name = to_snake_case(field.name)
294
+
295
+ if field.optional?
296
+ "#{field_name}: Optional[#{py_type}] = None"
297
+ elsif field.nullable?
298
+ "#{field_name}: Optional[#{py_type}]"
299
+ else
300
+ default = get_field_default(field)
301
+ if default
302
+ "#{field_name}: #{py_type} = #{default}"
303
+ else
304
+ "#{field_name}: #{py_type}"
305
+ end
306
+ end
307
+ end
308
+
309
+ def build_dataclass(target_schema)
310
+ lines = []
311
+
312
+ lines << "@dataclass"
313
+ lines << "class #{target_schema.name}:"
314
+
315
+ # Required fields first, then optional
316
+ required_fields = target_schema.fields.reject { |f| f.optional? || f.nullable? }
317
+ optional_fields = target_schema.fields.select { |f| f.optional? || f.nullable? }
318
+
319
+ if target_schema.fields.empty?
320
+ lines << " pass"
321
+ else
322
+ required_fields.each do |field|
323
+ py_type = field_to_python(field)
324
+ lines << " #{to_snake_case(field.name)}: #{py_type}"
325
+ end
326
+
327
+ optional_fields.each do |field|
328
+ py_type = field_to_python(field)
329
+ lines << " #{to_snake_case(field.name)}: Optional[#{py_type}] = None"
330
+ end
331
+ end
332
+
333
+ lines.join("\n")
334
+ end
335
+
336
+ def build_typed_dict(target_schema)
337
+ lines = []
338
+
339
+ # Check if we need total=False for optional fields
340
+ has_optional = target_schema.fields.any? { |f| f.optional? }
341
+
342
+ if has_optional
343
+ # Create two TypedDicts - required and optional
344
+ required_fields = target_schema.fields.reject(&:optional?)
345
+ optional_fields = target_schema.fields.select(&:optional?)
346
+
347
+ lines << "class #{target_schema.name}Required(TypedDict):"
348
+ if required_fields.empty?
349
+ lines << " pass"
350
+ else
351
+ required_fields.each do |field|
352
+ py_type = field_to_python(field)
353
+ lines << " #{to_snake_case(field.name)}: #{py_type}"
354
+ end
355
+ end
356
+ lines << ""
357
+
358
+ lines << "class #{target_schema.name}(#{target_schema.name}Required, total=False):"
359
+ if optional_fields.empty?
360
+ lines << " pass"
361
+ else
362
+ optional_fields.each do |field|
363
+ py_type = field_to_python(field)
364
+ lines << " #{to_snake_case(field.name)}: #{py_type}"
365
+ end
366
+ end
367
+ else
368
+ lines << "class #{target_schema.name}(TypedDict):"
369
+ if target_schema.fields.empty?
370
+ lines << " pass"
371
+ else
372
+ target_schema.fields.each do |field|
373
+ py_type = field_to_python(field)
374
+ nullable = " | None" if field.nullable?
375
+ lines << " #{to_snake_case(field.name)}: #{py_type}#{nullable}"
376
+ end
377
+ end
378
+ end
379
+
380
+ lines.join("\n")
381
+ end
382
+
383
+ def build_imports(types_used, style)
384
+ lines = []
385
+
386
+ case style
387
+ when :pydantic
388
+ lines << "from typing import Optional, List, Dict, Any, Tuple"
389
+ lines << "from pydantic import BaseModel, Field"
390
+
391
+ # Add specific imports
392
+ PYDANTIC_IMPORTS.each do |type, import_stmt|
393
+ lines << import_stmt if types_used.include?(type)
394
+ end
395
+
396
+ # Add Literal for enums
397
+ if types_used.include?("Literal")
398
+ lines << "from typing import Literal"
399
+ end
400
+
401
+ when :dataclass
402
+ lines << "from dataclasses import dataclass, field"
403
+ lines << "from typing import Optional, List, Dict, Any, Tuple"
404
+
405
+ PYDANTIC_IMPORTS.each do |type, import_stmt|
406
+ next if %w[EmailStr HttpUrl IPvAnyAddress].include?(type)
407
+ lines << import_stmt if types_used.include?(type)
408
+ end
409
+
410
+ when :typed_dict
411
+ lines << "from typing import TypedDict, Optional, List, Dict, Any, Tuple"
412
+
413
+ PYDANTIC_IMPORTS.each do |type, import_stmt|
414
+ next if %w[EmailStr HttpUrl IPvAnyAddress].include?(type)
415
+ lines << import_stmt if types_used.include?(type)
416
+ end
417
+ end
418
+
419
+ lines.uniq.join("\n")
420
+ end
421
+
422
+ def collect_types_used
423
+ types = Set.new
424
+ schema.fields.each do |field|
425
+ py_type = TYPE_MAPPINGS[field.type_name]
426
+ types << py_type if py_type
427
+ types << "Literal" if field.type_name == "enum"
428
+ end
429
+ types
430
+ end
431
+
432
+ def collect_referenced_schemas
433
+ schemas = []
434
+ schema.fields.each do |field|
435
+ collect_field_schemas(field, schemas)
436
+ end
437
+ schemas.uniq(&:name)
438
+ end
439
+
440
+ def collect_field_schemas(field, schemas)
441
+ type_name = field.type_name
442
+
443
+ if schema_reference?(type_name)
444
+ ref_schema = get_schema(type_name)
445
+ if ref_schema && !schemas.any? { |s| s.name == ref_schema.name }
446
+ schemas << ref_schema
447
+ ref_schema.fields.each { |f| collect_field_schemas(f, schemas) }
448
+ end
449
+ end
450
+
451
+ if type_name == "array"
452
+ element = field.type_args[:element]
453
+ if element && schema_reference?(element.to_s)
454
+ ref_schema = get_schema(element.to_s)
455
+ if ref_schema && !schemas.any? { |s| s.name == ref_schema.name }
456
+ schemas << ref_schema
457
+ ref_schema.fields.each { |f| collect_field_schemas(f, schemas) }
458
+ end
459
+ end
460
+ end
461
+ end
462
+
463
+ def field_to_python(field)
464
+ type_name = field.type_name
465
+ type_args = field.type_args
466
+
467
+ case type_name
468
+ when "enum"
469
+ build_enum_type(type_args)
470
+ when "const"
471
+ build_const_type(type_args)
472
+ when "array"
473
+ build_array_type(type_args)
474
+ when "one_of"
475
+ build_union_type(type_args)
476
+ when "map_by_field"
477
+ "Dict[str, Any]"
478
+ else
479
+ if TYPE_MAPPINGS[type_name]
480
+ TYPE_MAPPINGS[type_name]
481
+ elsif schema_reference?(type_name)
482
+ type_name
483
+ else
484
+ # Most Synthra types generate strings, so an unmapped, non-structural
485
+ # type is a string rather than Any.
486
+ "str"
487
+ end
488
+ end
489
+ end
490
+
491
+ def build_enum_type(args)
492
+ values = args[:values]&.map { |v| v.respond_to?(:value) ? v.value : v } || []
493
+ "Literal[#{values.map { |v| "\"#{v}\"" }.join(', ')}]"
494
+ end
495
+
496
+ def build_const_type(args)
497
+ value = args[:value]
498
+ case value
499
+ when String
500
+ "Literal[\"#{value}\"]"
501
+ when TrueClass, FalseClass
502
+ "Literal[#{value}]"
503
+ when Numeric
504
+ "Literal[#{value}]"
505
+ else
506
+ "Literal[\"#{value}\"]"
507
+ end
508
+ end
509
+
510
+ def build_array_type(args)
511
+ element = args[:element]
512
+ if element
513
+ if schema_reference?(element.to_s)
514
+ "List[#{element}]"
515
+ elsif TYPE_MAPPINGS[element.to_s]
516
+ "List[#{TYPE_MAPPINGS[element.to_s]}]"
517
+ else
518
+ # Unknown element types are string-producing types (e.g. array(string)).
519
+ "List[str]"
520
+ end
521
+ else
522
+ "List[Any]"
523
+ end
524
+ end
525
+
526
+ def build_union_type(args)
527
+ schemas = args[:schemas] || []
528
+ types = schemas.map do |s|
529
+ schema_name = s.is_a?(Hash) ? (s[:schema] || s["schema"]) : s.to_s
530
+ schema_reference?(schema_name) ? schema_name : "Any"
531
+ end
532
+ "Union[#{types.join(', ')}]"
533
+ end
534
+
535
+ def get_field_default(field)
536
+ type_name = field.type_name
537
+
538
+ case type_name
539
+ when "enum"
540
+ values = field.type_args[:values] || []
541
+ first = values.first
542
+ first ? "\"#{first.respond_to?(:value) ? first.value : first}\"" : nil
543
+ when "const"
544
+ value = field.type_args[:value]
545
+ value.is_a?(String) ? "\"#{value}\"" : value.to_s
546
+ else
547
+ nil
548
+ end
549
+ end
550
+
551
+ def to_snake_case(str)
552
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
553
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
554
+ .downcase
555
+ end
556
+ end
557
+ # :nocov:
558
+ end
559
+ end
560
+