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,533 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Registry
6
+ # =============================================================================
7
+ #
8
+ # The Registry class manages loading and storing multiple schema definitions.
9
+ # It's essential for handling cross-schema references (Ref(User.id)) and
10
+ # for organizing related schemas.
11
+ #
12
+ # @example Loading multiple schemas
13
+ # registry = Synthra::Registry.new
14
+ # registry.load_file("schemas/user.dsl")
15
+ # registry.load_file("schemas/order.dsl")
16
+ #
17
+ # user = registry.schema("User").generate
18
+ # order = registry.schema("Order").generate(registry: registry)
19
+ #
20
+ # =============================================================================
21
+
22
+
23
+ module Synthra
24
+
25
+ # Registry for storing and managing multiple schemas
26
+ #
27
+ # The Registry provides a central store for schema definitions, enabling:
28
+ # - Loading multiple DSL files
29
+ # - Cross-schema references (Ref(User.id))
30
+ # - Schema lookup by name
31
+ # - Enumeration of all schemas
32
+ #
33
+ # When generating data with cross-schema references, pass the registry
34
+ # to the generate method to enable reference resolution.
35
+ #
36
+ # @example Basic usage
37
+ # registry = Synthra::Registry.new
38
+ # registry.load_file("schemas/user.dsl")
39
+ #
40
+ # user_schema = registry.schema("User")
41
+ # user = user_schema.generate
42
+ #
43
+ # @example Multiple schemas with references
44
+ # registry = Synthra::Registry.new
45
+ # registry.load_file("schemas/user.dsl") # User: id, name, ...
46
+ # registry.load_file("schemas/order.dsl") # Order: user_id: Ref(User.id)
47
+ #
48
+ # order = registry.schema("Order").generate(registry: registry)
49
+ # # order["user_id"] will be a generated User.id
50
+ #
51
+ # @example Iterating over schemas
52
+ # registry.each do |name, schema|
53
+ # puts "Schema: #{name} with #{schema.fields.length} fields"
54
+ # end
55
+ #
56
+
57
+ class Registry
58
+
59
+ # Create a new empty registry
60
+ #
61
+ # @return [Registry] a new registry instance
62
+ #
63
+ # @example
64
+ # registry = Synthra::Registry.new
65
+ # registry.size # => 0
66
+ #
67
+
68
+ def initialize
69
+ @schemas = {}
70
+ @mutex = Mutex.new
71
+ @ast_cache = {}
72
+ @cache_mutex = Mutex.new
73
+ @max_cache_size = Synthra.configuration.registry_cache_size
74
+ end
75
+
76
+
77
+ # Load schemas from a DSL file
78
+ #
79
+ # Reads and parses the DSL file, adding all schemas defined in it
80
+ # to the registry. Existing schemas with the same name will be
81
+ # overwritten.
82
+ #
83
+ # @param path [String] path to the DSL file (relative or absolute)
84
+ # @param validate [Boolean] whether to validate copy() paths (default: config setting)
85
+ # @return [Array<Schema>] the schemas that were loaded
86
+ # @raise [Errors::ParseError] if the DSL syntax is invalid
87
+ # @raise [Errors::SyntaxError] if there are syntax errors
88
+ # @raise [Errors::PathError] if copy() path validation fails
89
+ # @raise [Errno::ENOENT] if the file doesn't exist
90
+ #
91
+ # @example
92
+ # registry.load_file("schemas/user.dsl")
93
+ # # => [#<Schema User fields=5 behaviors=0>]
94
+ #
95
+ # @example Handle errors
96
+ # begin
97
+ # registry.load_file("invalid.dsl")
98
+ # rescue Synthra::ParseError => e
99
+ # puts "Error at line #{e.line}: #{e.message}"
100
+ # end
101
+ #
102
+ # @example Skip validation
103
+ # registry.load_file("schema.dsl", validate: false)
104
+ #
105
+
106
+ def load_file(path, validate: Synthra.configuration.validate_paths_on_load)
107
+ schemas = @mutex.synchronize do
108
+ load_file_unsafe(path)
109
+ end
110
+
111
+ # Validate paths outside mutex to avoid deadlock
112
+ validate_paths! if validate
113
+
114
+ schemas
115
+ end
116
+
117
+ private
118
+
119
+ def load_file_unsafe(path)
120
+
121
+ # Check cache first
122
+ cache_key = File.expand_path(path)
123
+ mtime = File.mtime(path) if File.exist?(path)
124
+
125
+ cached = @cache_mutex.synchronize do
126
+ cache_entry = @ast_cache[cache_key]
127
+ if cache_entry && cache_entry[:mtime] == mtime
128
+
129
+ # Update access time for LRU tracking
130
+ cache_entry[:accessed_at] = Time.now
131
+ cache_entry[:ast_nodes]
132
+ else
133
+ nil
134
+ end
135
+ end
136
+
137
+ if cached
138
+
139
+ # Use cached AST. Normalize through extract_schema_nodes so the new Simulyra
140
+ # (ProgramNode) syntax works here too — load_string already does this; load_file
141
+ # used to blindly #map the raw parse result and broke on every v2 .dsl file.
142
+ extract_schema_nodes(cached).map do |node|
143
+ schema = node.to_schema
144
+ @schemas[schema.name] = schema
145
+ schema
146
+ end
147
+ else
148
+
149
+ # Parse and cache
150
+ content = File.read(path)
151
+ parser = Parser::Parser.new
152
+ ast_nodes = parser.parse(content)
153
+
154
+
155
+ # Cache the AST with LRU eviction
156
+ @cache_mutex.synchronize do
157
+
158
+ # Limit cache size using LRU (Least Recently Used) eviction
159
+ if @ast_cache.size >= @max_cache_size
160
+
161
+ # Remove least recently used entry
162
+ oldest_key = @ast_cache.min_by { |_, v| v[:accessed_at] || Time.at(0) }[0]
163
+ @ast_cache.delete(oldest_key)
164
+ end
165
+ @ast_cache[cache_key] = { ast_nodes: ast_nodes, mtime: mtime, accessed_at: Time.now }
166
+ end
167
+
168
+ extract_schema_nodes(ast_nodes).map do |node|
169
+ schema = node.to_schema
170
+ @schemas[schema.name] = schema
171
+ schema
172
+ end
173
+ end
174
+ end
175
+
176
+ public
177
+
178
+
179
+ # Load schemas from all DSL files in a directory
180
+ #
181
+ # Loads and parses all .dsl files in the specified directory, adding
182
+ # all schemas defined in them to the registry. Existing schemas with
183
+ # the same name will be overwritten.
184
+ #
185
+ # @param dir [String] directory path containing .dsl files
186
+ # @param validate [Boolean] whether to validate copy() paths (default: config setting)
187
+ # @return [Array<Schema>] all schemas that were loaded
188
+ # @raise [Errno::ENOENT] if the directory doesn't exist
189
+ # @raise [Errors::ParseError] if any DSL file has syntax errors
190
+ # @raise [Errors::PathError] if copy() path validation fails
191
+ #
192
+ # @example
193
+ # registry.load_dir("schemas/")
194
+ # # => [#<Schema User>, #<Schema Order>, #<Schema Product>]
195
+ #
196
+ # @example Handle errors
197
+ # begin
198
+ # registry.load_dir("nonexistent/")
199
+ # rescue Errno::ENOENT => e
200
+ # puts "Directory not found: #{e.message}"
201
+ # end
202
+ #
203
+ # @example Skip validation
204
+ # registry.load_dir("schemas/", validate: false)
205
+ #
206
+
207
+ def load_dir(dir, validate: Synthra.configuration.validate_paths_on_load)
208
+ schemas = @mutex.synchronize do
209
+
210
+ # Validate and expand directory path
211
+ dir = File.expand_path(dir)
212
+ raise Errno::ENOENT, dir unless Dir.exist?(dir)
213
+
214
+
215
+ # Find all .dsl files
216
+ pattern = File.join(dir, "*.dsl")
217
+ files = Dir.glob(pattern)
218
+
219
+
220
+ # Load each file
221
+ all_schemas = []
222
+ files.each do |file|
223
+ # Call load_file_unsafe to avoid double synchronization (we're already in @mutex)
224
+ all_schemas.concat(load_file_unsafe(file))
225
+ end
226
+
227
+ all_schemas
228
+ end
229
+
230
+ # Validate paths outside mutex to avoid deadlock
231
+ validate_paths! if validate
232
+
233
+ schemas
234
+ end
235
+
236
+
237
+ # Load schemas from a DSL string
238
+ #
239
+ # Parses the DSL source code and adds all schemas to the registry.
240
+ # Useful for testing or when DSL content is dynamically generated.
241
+ #
242
+ # @param source [String] DSL source code
243
+ # @param validate [Boolean] whether to validate copy() paths (default: config setting)
244
+ # @return [Array<Schema>] the schemas that were parsed
245
+ # @raise [Errors::ParseError] if the DSL syntax is invalid
246
+ # @raise [Errors::PathError] if copy() path validation fails
247
+ #
248
+ # @example
249
+ # dsl = <<~DSL
250
+ # User:
251
+ # id: uuid
252
+ # name: name
253
+ # DSL
254
+ #
255
+ # schemas = registry.load_string(dsl)
256
+ # schemas.first.name # => "User"
257
+ #
258
+ # @example Skip validation
259
+ # schemas = registry.load_string(dsl, validate: false)
260
+ #
261
+
262
+ def load_string(source, validate: Synthra.configuration.validate_paths_on_load)
263
+ schemas = @mutex.synchronize do
264
+ parser = Parser::Parser.new
265
+ parse_result = parser.parse(source)
266
+
267
+ # Handle both legacy (array of SchemaNode) and Simulyra (ProgramNode) syntax
268
+ ast_nodes = extract_schema_nodes(parse_result)
269
+
270
+ ast_nodes.map do |node|
271
+ schema = node.to_schema
272
+ @schemas[schema.name] = schema
273
+ schema
274
+ end
275
+ end
276
+
277
+ # Validate paths outside mutex to avoid deadlock
278
+ validate_paths! if validate
279
+
280
+ schemas
281
+ end
282
+
283
+ private
284
+
285
+ # Extract schema nodes from parse result
286
+ # Handles both legacy (Array) and Simulyra (ProgramNode) syntax
287
+ #
288
+ # @param parse_result [Array, ProgramNode] parsed result
289
+ # @return [Array<SchemaNode>] array of schema nodes
290
+ #
291
+ def extract_schema_nodes(parse_result)
292
+ if parse_result.is_a?(Parser::AST::ProgramNode)
293
+ # Simulyra DSL syntax - combine all schemas
294
+ all_schemas = []
295
+ all_schemas.concat(parse_result.schemas) if parse_result.schemas
296
+ all_schemas.concat(parse_result.legacy_schemas) if parse_result.legacy_schemas
297
+ all_schemas
298
+ elsif parse_result.is_a?(Array)
299
+ # Legacy syntax - already an array of schema nodes
300
+ parse_result
301
+ else
302
+ # Single node - wrap in array
303
+ [parse_result].compact
304
+ end
305
+ end
306
+
307
+ public
308
+
309
+
310
+ # Get a schema by name
311
+ #
312
+ # Retrieves a schema from the registry. Raises KeyError if not found.
313
+ #
314
+ # @param name [String, Symbol] schema name
315
+ # @return [Schema] the schema
316
+ # @raise [KeyError] if schema is not found
317
+ #
318
+ # @example
319
+ # user_schema = registry.schema("User")
320
+ # user_schema.fields.length # => 5
321
+ #
322
+ # @example Handle missing schema
323
+ # begin
324
+ # registry.schema("NonExistent")
325
+ # rescue KeyError => e
326
+ # puts e.message # => "Schema not found: NonExistent"
327
+ # end
328
+ #
329
+
330
+ def schema(name)
331
+ @mutex.synchronize do
332
+ @schemas.fetch(name.to_s) do
333
+ raise KeyError, "Schema not found: #{name}"
334
+ end
335
+ end
336
+ end
337
+
338
+ # Register an already-built Schema object under a name.
339
+ #
340
+ # Counterpart to load_string/load_file for callers that construct a Schema directly (OpenAPI
341
+ # import, ActiveRecord inference, the CLI/API server). Was referenced in those paths but never
342
+ # defined, so every one of them raised NoMethodError.
343
+ #
344
+ # @param name [String, Symbol] the schema name
345
+ # @param schema [Schema] the schema to register
346
+ # @return [Schema] the registered schema
347
+ #
348
+ def register_schema(name, schema)
349
+ @mutex.synchronize do
350
+ @schemas[name.to_s] = schema
351
+ end
352
+ end
353
+
354
+
355
+ # Get all registered schemas as a hash
356
+ #
357
+ # Returns a copy of the internal schemas hash to prevent modification.
358
+ #
359
+ # @return [Hash<String, Schema>] schema name => Schema mapping
360
+ #
361
+ # @example
362
+ # all_schemas = registry.schemas
363
+ # all_schemas.keys # => ["User", "Order"]
364
+ #
365
+
366
+ def schemas
367
+ @mutex.synchronize do
368
+ @schemas.dup
369
+ end
370
+ end
371
+
372
+
373
+ # Check if a schema exists
374
+ #
375
+ # @param name [String, Symbol] schema name to check
376
+ # @return [Boolean] true if the schema exists
377
+ #
378
+ # @example
379
+ # registry.schema?("User") # => true
380
+ # registry.schema?("Unknown") # => false
381
+ #
382
+
383
+ def schema?(name)
384
+ @mutex.synchronize do
385
+ @schemas.key?(name.to_s)
386
+ end
387
+ end
388
+
389
+
390
+ # Unregister a schema by name
391
+ #
392
+ # Removes a single schema from the registry.
393
+ #
394
+ # @param name [String] schema name to remove
395
+ # @return [Schema, nil] the removed schema, or nil if not found
396
+ #
397
+ # @example
398
+ # registry.unregister("User")
399
+ #
400
+ def unregister(name)
401
+ @mutex.synchronize do
402
+ @schemas.delete(name.to_s)
403
+ end
404
+ end
405
+
406
+
407
+ # Clear all registered schemas
408
+ #
409
+ # Removes all schemas from the registry.
410
+ #
411
+ # @return [void]
412
+ #
413
+ # @example
414
+ # registry.clear
415
+ # registry.size # => 0
416
+ #
417
+
418
+ def clear
419
+ @mutex.synchronize do
420
+ @schemas.clear
421
+ @cache_mutex.synchronize do
422
+ @ast_cache.clear
423
+ end
424
+ end
425
+ end
426
+
427
+
428
+ # Get the number of registered schemas
429
+ #
430
+ # @return [Integer] number of schemas in the registry
431
+ #
432
+ # @example
433
+ # registry.load_file("user.dsl")
434
+ # registry.size # => 1
435
+ #
436
+
437
+ def size
438
+ @mutex.synchronize do
439
+ @schemas.size
440
+ end
441
+ end
442
+
443
+
444
+ # Alias for size
445
+ # @return [Integer]
446
+ alias count size
447
+
448
+
449
+ # Alias for size
450
+ # @return [Integer]
451
+ alias length size
452
+
453
+
454
+ # Iterate over all schemas
455
+ #
456
+ # Yields each schema name and schema object to the block.
457
+ #
458
+ # @yield [name, schema] block to execute for each schema
459
+ # @yieldparam name [String] the schema name
460
+ # @yieldparam schema [Schema] the schema object
461
+ # @return [Enumerator, void] returns Enumerator if no block given
462
+ #
463
+ # @example
464
+ # registry.each do |name, schema|
465
+ # puts "#{name}: #{schema.fields.length} fields"
466
+ # end
467
+ #
468
+ # @example Get as array
469
+ # pairs = registry.each.to_a
470
+ # # => [["User", #<Schema>], ["Order", #<Schema>]]
471
+ #
472
+
473
+ def each(&block)
474
+ @mutex.synchronize do
475
+ @schemas.each(&block)
476
+ end
477
+ end
478
+
479
+
480
+ # Get all schema names
481
+ #
482
+ # @return [Array<String>] list of schema names
483
+ #
484
+ # @example
485
+ # registry.names # => ["User", "Order", "Product"]
486
+ #
487
+
488
+ def names
489
+ @mutex.synchronize do
490
+ @schemas.keys
491
+ end
492
+ end
493
+
494
+
495
+ # Validate all copy() paths in the registry
496
+ #
497
+ # Checks that all copy() type references point to valid paths
498
+ # in the schema structure. This catches typos and invalid
499
+ # references at load time rather than at generation time.
500
+ #
501
+ # @return [Array<Errors::PathError>] list of validation errors (empty if valid)
502
+ #
503
+ # @example
504
+ # errors = registry.validate_paths
505
+ # errors.each { |e| puts e.message }
506
+ #
507
+
508
+ def validate_paths
509
+ # Don't hold mutex during validation - the validator caches schemas
510
+ # to avoid deadlock
511
+ validator = Validator::PathValidator.new(self)
512
+ validator.validate
513
+ end
514
+
515
+
516
+ # Validate all copy() paths, raising on first error
517
+ #
518
+ # @raise [Errors::PathError] if any invalid path is found
519
+ # @return [true] if all paths are valid
520
+ #
521
+ # @example
522
+ # registry.load_file("schema.dsl")
523
+ # registry.validate_paths! # Raises if copy() paths are invalid
524
+ #
525
+
526
+ def validate_paths!
527
+ # Don't hold mutex during validation - the validator caches schemas
528
+ # to avoid deadlock
529
+ validator = Validator::PathValidator.new(self)
530
+ validator.validate!
531
+ end
532
+ end
533
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Data Relationships Support
5
+ #
6
+ # Provides expressive relationship types for schemas:
7
+ # - belongs_to(Schema) - Required reference
8
+ # - has_one(Schema) - Optional single reference
9
+ # - has_many(Schema, count) - Collection reference
10
+ #
11
+ # @example DSL syntax
12
+ # Order:
13
+ # id: uuid
14
+ # user: belongs_to(User)
15
+ # items: has_many(OrderItem, 1..10)
16
+ # shipping_address?: has_one(Address)
17
+ #
18
+ module Relationships
19
+ # Relationship type definitions
20
+ TYPES = {
21
+ belongs_to: { required: true, cardinality: :one },
22
+ has_one: { required: false, cardinality: :one },
23
+ has_many: { required: true, cardinality: :many }
24
+ }.freeze
25
+
26
+ # Parse a relationship type from DSL
27
+ #
28
+ # @param type_str [String] e.g., "belongs_to(User)"
29
+ # @return [Hash, nil] parsed relationship or nil if not a relationship
30
+ #
31
+ def self.parse(type_str)
32
+ return nil unless type_str.is_a?(String)
33
+
34
+ TYPES.keys.each do |rel_type|
35
+ pattern = /^#{rel_type}\((\w+)(?:,\s*(\d+)\.\.(\d+))?\)$/
36
+ if (match = type_str.match(pattern))
37
+ return {
38
+ type: rel_type,
39
+ target_schema: match[1],
40
+ min_count: match[2]&.to_i || 1,
41
+ max_count: match[3]&.to_i || 1,
42
+ **TYPES[rel_type]
43
+ }
44
+ end
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ # Check if a type string is a relationship
51
+ #
52
+ # @param type_str [String]
53
+ # @return [Boolean]
54
+ #
55
+ def self.relationship?(type_str)
56
+ return false unless type_str.is_a?(String)
57
+ TYPES.keys.any? { |t| type_str.start_with?("#{t}(") }
58
+ end
59
+
60
+ # Convert relationship to standard Synthra type
61
+ #
62
+ # @param relationship [Hash] parsed relationship
63
+ # @return [String] equivalent standard type
64
+ #
65
+ def self.to_standard_type(relationship)
66
+ case relationship[:cardinality]
67
+ when :one
68
+ relationship[:target_schema]
69
+ when :many
70
+ "array(#{relationship[:target_schema]}, #{relationship[:min_count]}..#{relationship[:max_count]})"
71
+ end
72
+ end
73
+ end
74
+
75
+ module Types
76
+ module Core
77
+ # BelongsTo type - required single reference
78
+ class BelongsTo < Base
79
+ def generate_random(rng, context, args)
80
+ target_schema = args[:schema] || args[:target]
81
+ return nil unless target_schema
82
+
83
+ if context.registry&.schema?(target_schema)
84
+ referenced_schema = context.registry.schema(target_schema)
85
+ id_field = referenced_schema.field("id")
86
+ if id_field
87
+ # Generate just the ID
88
+ Generator::Engine.new(referenced_schema, registry: context.registry)
89
+ .generate(mode: :random)["id"]
90
+ end
91
+ else
92
+ # Fallback: generate a UUID
93
+ SecureRandom.uuid
94
+ end
95
+ end
96
+
97
+ def generate_edge(rng, context, args)
98
+ generate_random(rng, context, args)
99
+ end
100
+
101
+ def generate_invalid(rng, context, args)
102
+ rng.sample([nil, "", "invalid_id", 0, -1])
103
+ end
104
+
105
+ def generate_hostile(rng, context, args)
106
+ rng.sample([
107
+ "' OR '1'='1",
108
+ "1; DROP TABLE users;--",
109
+ "../../../etc/passwd",
110
+ "<script>alert(1)</script>"
111
+ ])
112
+ end
113
+ end
114
+
115
+ # HasOne type - optional single reference (generates full object)
116
+ class HasOne < Base
117
+ def generate_random(rng, context, args)
118
+ target_schema = args[:schema] || args[:target]
119
+ return nil unless target_schema
120
+
121
+ if context.registry&.schema?(target_schema)
122
+ referenced_schema = context.registry.schema(target_schema)
123
+ referenced_schema.generate(registry: context.registry, mode: :random)
124
+ end
125
+ end
126
+
127
+ def generate_edge(rng, context, args)
128
+ # Edge case: nil or empty
129
+ rng.boolean(0.5) ? nil : generate_random(rng, context, args)
130
+ end
131
+
132
+ def generate_invalid(rng, context, args)
133
+ rng.sample([nil, {}, [], "not_an_object"])
134
+ end
135
+
136
+ def generate_hostile(rng, context, args)
137
+ { "injection" => "' OR '1'='1" }
138
+ end
139
+ end
140
+
141
+ # HasMany type - collection reference
142
+ class HasMany < Base
143
+ def generate_random(rng, context, args)
144
+ target_schema = args[:schema] || args[:target]
145
+ min_count = args[:min] || 1
146
+ max_count = args[:max] || 5
147
+ count = rng.int(min_count, max_count)
148
+
149
+ return [] unless target_schema
150
+
151
+ if context.registry&.schema?(target_schema)
152
+ referenced_schema = context.registry.schema(target_schema)
153
+ count.times.map do
154
+ referenced_schema.generate(registry: context.registry, mode: :random)
155
+ end
156
+ else
157
+ []
158
+ end
159
+ end
160
+
161
+ def generate_edge(rng, context, args)
162
+ # Edge cases: empty array, single item, max items
163
+ edge_counts = [0, 1, args[:max] || 10]
164
+ count = rng.sample(edge_counts)
165
+
166
+ target_schema = args[:schema] || args[:target]
167
+ return [] unless target_schema && count > 0
168
+
169
+ if context.registry&.schema?(target_schema)
170
+ referenced_schema = context.registry.schema(target_schema)
171
+ count.times.map do
172
+ referenced_schema.generate(registry: context.registry, mode: :edge)
173
+ end
174
+ else
175
+ []
176
+ end
177
+ end
178
+
179
+ def generate_invalid(rng, context, args)
180
+ rng.sample([nil, "not_an_array", {}, [nil, nil]])
181
+ end
182
+
183
+ def generate_hostile(rng, context, args)
184
+ # Generate array with hostile payloads
185
+ [
186
+ { "xss" => "<script>alert(1)</script>" },
187
+ { "sqli" => "'; DROP TABLE users;--" }
188
+ ]
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end