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,615 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ # =============================================================================
6
+
7
+ # Synthra Path Validator
8
+ # =============================================================================
9
+ #
10
+ # Validates copy() path references against schema structure at parse/load time.
11
+ # This catches path typos and invalid references before data generation.
12
+ #
13
+ # @example Validate paths in a registry
14
+ # validator = PathValidator.new(registry)
15
+ # validator.validate!
16
+ #
17
+ # =============================================================================
18
+
19
+
20
+ module Synthra
21
+ module Validator
22
+
23
+ # Validates copy() path references against schema structure
24
+ #
25
+ # The PathValidator scans all schemas in a registry for copy() type
26
+ # fields and validates that the referenced paths actually exist.
27
+ # It handles:
28
+ # - Direct field references (e.g., "user_id")
29
+ # - Nested field references (e.g., "address.city")
30
+ # - map_by_field alias references (e.g., "users.sender.id")
31
+ #
32
+ # @example Validate all paths in a registry
33
+ # validator = PathValidator.new(registry)
34
+ # validator.validate! # Raises PathError if invalid paths found
35
+ #
36
+ # @example Get validation results without raising
37
+ # errors = validator.validate
38
+ # errors.each { |e| puts e.message }
39
+ #
40
+
41
+ class PathValidator
42
+ # @param registry [Registry] the registry containing schemas to validate
43
+ def initialize(registry)
44
+ @registry = registry
45
+ @path_index = {}
46
+ @copy_paths = []
47
+ # Cache schemas to avoid mutex deadlock when registry calls back
48
+ @schemas_cache = registry.schemas
49
+ end
50
+
51
+ # Validate all copy() paths in the registry
52
+ #
53
+ # @return [Array<PathError, CycleError>] list of validation errors (empty if valid)
54
+ #
55
+ def validate
56
+ build_path_index
57
+ collect_copy_paths
58
+
59
+ errors = []
60
+ errors.concat(validate_paths)
61
+ errors.concat(validate_field_order)
62
+ errors.concat(validate_ref_targets)
63
+ errors.concat(detect_cycles)
64
+ errors
65
+ end
66
+
67
+ # Validate all copy() paths, raising on first error
68
+ #
69
+ # @raise [Errors::PathError] if any invalid path is found
70
+ # @return [true] if all paths are valid
71
+ #
72
+ def validate!
73
+ errors = validate
74
+ raise errors.first if errors.any?
75
+
76
+ true
77
+ end
78
+
79
+ private
80
+
81
+ # Build an index of all available field paths in all schemas
82
+ #
83
+ # @return [void]
84
+ #
85
+ def build_path_index
86
+ @path_index = {}
87
+
88
+ @schemas_cache.each do |name, schema|
89
+ # Track visited schemas per top-level schema to detect cycles
90
+ visited = Set.new
91
+ build_schema_paths(name, schema, "", visited)
92
+ end
93
+ end
94
+
95
+ # Build paths for a single schema
96
+ #
97
+ # @param name [String] schema name
98
+ # @param schema [Schema] the schema
99
+ # @param prefix [String] path prefix for nested fields
100
+ # @param visited [Set] schemas already visited in this path
101
+ # @return [void]
102
+ #
103
+ def build_schema_paths(name, schema, prefix, visited)
104
+ # Avoid infinite recursion on circular references
105
+ return if visited.include?(name)
106
+ visited.add(name)
107
+
108
+ schema.fields.each do |field|
109
+ field_path = prefix.empty? ? field.name : "#{prefix}.#{field.name}"
110
+ @path_index[field_path] = { schema: name, field: field }
111
+
112
+ # Handle nested schema types
113
+ if nested_schema?(field)
114
+ nested_name = field.type_name
115
+ if schema_exists?(nested_name) && !visited.include?(nested_name)
116
+ nested_schema = get_schema(nested_name)
117
+ build_schema_paths(nested_name, nested_schema, field_path, visited.dup)
118
+ end
119
+ end
120
+
121
+ # Handle map_by_field entries
122
+ if map_by_field?(field)
123
+ build_map_by_field_paths(field, field_path, visited)
124
+ end
125
+
126
+ # Handle array types with nested schemas
127
+ if array_with_nested_schema?(field)
128
+ element_type = field.type_args[:element]
129
+ if schema_exists?(element_type.to_s) && !visited.include?(element_type.to_s)
130
+ nested_schema = get_schema(element_type.to_s)
131
+ # Arrays can be accessed by index like "items.0.name"
132
+ build_schema_paths(element_type.to_s, nested_schema, "#{field_path}.0", visited.dup)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ # Build paths for map_by_field entries
139
+ #
140
+ # @param field [Field] the map_by_field field
141
+ # @param prefix [String] path prefix
142
+ # @param visited [Set] schemas already visited in this path
143
+ # @return [void]
144
+ #
145
+ def build_map_by_field_paths(field, prefix, visited)
146
+ entries = field.type_args[:entries] || {}
147
+
148
+ entries.each do |entry_name, entry_info|
149
+ entry_path = "#{prefix}.#{entry_name}"
150
+ @path_index[entry_path] = { schema: nil, field: field, entry: entry_name }
151
+
152
+ # Get the schema name (handle both string and hash formats)
153
+ schema_name = if entry_info.is_a?(Hash)
154
+ entry_info[:schema] || entry_info["schema"]
155
+ else
156
+ entry_info.to_s
157
+ end
158
+
159
+ if schema_exists?(schema_name) && !visited.include?(schema_name)
160
+ nested_schema = get_schema(schema_name)
161
+ build_schema_paths(schema_name, nested_schema, entry_path, visited.dup)
162
+ end
163
+ end
164
+ end
165
+
166
+ # Collect all copy() paths from all schemas
167
+ #
168
+ # @return [void]
169
+ #
170
+ def collect_copy_paths
171
+ @copy_paths = []
172
+ @collected_schemas = Set.new
173
+
174
+ @schemas_cache.each do |name, schema|
175
+ collect_schema_copy_paths(name, schema)
176
+ end
177
+ end
178
+
179
+ # Collect copy() paths from a single schema
180
+ #
181
+ # @param name [String] schema name
182
+ # @param schema [Schema] the schema
183
+ # @return [void]
184
+ #
185
+ def collect_schema_copy_paths(name, schema)
186
+ # Avoid infinite recursion on circular references
187
+ return if @collected_schemas.include?(name)
188
+ @collected_schemas.add(name)
189
+
190
+ schema.fields.each do |field|
191
+ if copy_type?(field)
192
+ path = extract_copy_path(field)
193
+ @copy_paths << {
194
+ path: path,
195
+ schema: name,
196
+ field: field.name,
197
+ line: nil # Line info not available on Field
198
+ } if path
199
+ end
200
+
201
+ # Also check nested schemas recursively
202
+ if nested_schema?(field)
203
+ nested_name = field.type_name
204
+ if schema_exists?(nested_name) && !@collected_schemas.include?(nested_name)
205
+ nested_schema = get_schema(nested_name)
206
+ collect_schema_copy_paths(nested_name, nested_schema)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ # Validate all collected copy() paths
213
+ #
214
+ # @return [Array<PathError>] list of validation errors
215
+ #
216
+ def validate_paths
217
+ errors = []
218
+
219
+ @copy_paths.each do |path_info|
220
+ path = path_info[:path]
221
+
222
+ # Skip validation if path contains parent context indicators
223
+ # (these need runtime resolution)
224
+ next if path_needs_runtime_resolution?(path)
225
+
226
+ unless path_exists?(path)
227
+ suggestions = find_similar_paths(path)
228
+ errors << Errors::PathError.new(
229
+ path,
230
+ available_paths: @path_index.keys.first(20),
231
+ suggestions: suggestions,
232
+ line: path_info[:line]
233
+ )
234
+ end
235
+ end
236
+
237
+ # Also validate map_by_field key fields
238
+ errors.concat(validate_map_by_field_keys)
239
+
240
+ errors
241
+ end
242
+
243
+ # Validate that copy() fields don't reference later-defined fields
244
+ #
245
+ # copy() can only reference fields that are defined BEFORE it in the schema.
246
+ # This is because fields are generated in definition order, so referencing
247
+ # a later field would result in nil.
248
+ #
249
+ # @return [Array<Errors::FieldOrderError>] list of field order errors
250
+ #
251
+ def validate_field_order
252
+ errors = []
253
+
254
+ @schemas_cache.each do |schema_name, schema|
255
+ field_names = schema.fields.map(&:name)
256
+
257
+ schema.fields.each_with_index do |field, field_index|
258
+ next unless copy_type?(field)
259
+
260
+ copy_path = extract_copy_path(field)
261
+ next unless copy_path
262
+
263
+ # Get the root field name from the path (e.g., "users.sender.id" -> "users")
264
+ root_field = copy_path.split(".").first
265
+ next unless root_field
266
+
267
+ # Check if the root field is defined in this schema
268
+ root_index = field_names.index(root_field)
269
+ next unless root_index # Field is from parent context or nested schema
270
+
271
+ # If the copy references a field defined AFTER it, that's a problem
272
+ if root_index >= field_index
273
+ errors << Errors::FieldOrderError.new(
274
+ field_name: field.name,
275
+ copy_path: copy_path,
276
+ schema_name: schema_name,
277
+ suggestion: "Move '#{root_field}' field before '#{field.name}' in schema '#{schema_name}'"
278
+ )
279
+ end
280
+ end
281
+ end
282
+
283
+ errors
284
+ end
285
+
286
+ # Validate that Ref() targets exist and have the referenced field
287
+ #
288
+ # @return [Array<PathError>] list of validation errors
289
+ #
290
+ def validate_ref_targets
291
+ errors = []
292
+
293
+ @schemas_cache.each do |schema_name, schema|
294
+ schema.fields.each do |field|
295
+ next unless ref_type?(field)
296
+
297
+ ref_schema = field.type_args[:schema] || field.type_args["schema"]
298
+ ref_field = field.type_args[:field] || field.type_args["field"]
299
+
300
+ next unless ref_schema
301
+
302
+ # Check if referenced schema exists
303
+ unless schema_exists?(ref_schema)
304
+ similar = Utils::StringDistance.similar_strings(ref_schema, @schemas_cache.keys, max_distance: 2, limit: 3)
305
+ errors << Errors::PathError.new(
306
+ "Ref(#{ref_schema}.#{ref_field})",
307
+ suggestions: similar.map { |s| "Ref(#{s}.#{ref_field || 'id'})" },
308
+ line: nil
309
+ )
310
+ next
311
+ end
312
+
313
+ # Check if referenced field exists in target schema
314
+ if ref_field
315
+ target_schema = get_schema(ref_schema)
316
+ field_names = target_schema.fields.map(&:name)
317
+
318
+ unless field_names.include?(ref_field)
319
+ similar = Utils::StringDistance.similar_strings(ref_field, field_names, max_distance: 2, limit: 3)
320
+ errors << Errors::PathError.new(
321
+ "#{ref_schema}.#{ref_field}",
322
+ suggestions: similar.map { |f| "Ref(#{ref_schema}.#{f})" },
323
+ line: nil
324
+ )
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ errors
331
+ end
332
+
333
+ # Validate that map_by_field key fields exist in referenced schemas
334
+ #
335
+ # @return [Array<PathError>] list of validation errors
336
+ #
337
+ def validate_map_by_field_keys
338
+ errors = []
339
+
340
+ @schemas_cache.each do |schema_name, schema|
341
+ schema.fields.each do |field|
342
+ next unless map_by_field?(field)
343
+
344
+ key_field = field.type_args[:key_field] || field.type_args["key_field"] || "id"
345
+ entries = field.type_args[:entries] || field.type_args["entries"] || {}
346
+
347
+ entries.each do |entry_name, entry_info|
348
+ # Get the schema name (handle both string and hash formats)
349
+ entry_schema_name = if entry_info.is_a?(Hash)
350
+ entry_info[:schema] || entry_info["schema"]
351
+ else
352
+ entry_info.to_s
353
+ end
354
+
355
+ next unless entry_schema_name && schema_exists?(entry_schema_name)
356
+
357
+ entry_schema = get_schema(entry_schema_name)
358
+ field_names = entry_schema.fields.map(&:name)
359
+
360
+ unless field_names.include?(key_field)
361
+ similar = Utils::StringDistance.similar_strings(key_field, field_names, max_distance: 2, limit: 3)
362
+ errors << Errors::PathError.new(
363
+ "#{key_field}' in schema '#{entry_schema_name}",
364
+ suggestions: similar.map { |f| "#{entry_schema_name}.#{f}" },
365
+ line: nil
366
+ )
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ errors
373
+ end
374
+
375
+ # Check if a path exists in the index
376
+ #
377
+ # @param path [String] the path to check
378
+ # @return [Boolean] true if path exists
379
+ #
380
+ def path_exists?(path)
381
+ return true if @path_index.key?(path)
382
+
383
+ # Try partial path matching (for nested paths that may be dynamically generated)
384
+ parts = path.split(".")
385
+ current_path = ""
386
+
387
+ parts.each_with_index do |part, idx|
388
+ current_path = idx.zero? ? part : "#{current_path}.#{part}"
389
+
390
+ # Check if this is the last part
391
+ if idx == parts.length - 1
392
+ return @path_index.key?(current_path)
393
+ end
394
+
395
+ # If path doesn't exist and we're not at the end, check if we have a dynamic path
396
+ next if @path_index.key?(current_path)
397
+
398
+ # Check for array index patterns (e.g., "0" for first element)
399
+ return true if part =~ /^\d+$/
400
+
401
+ return false
402
+ end
403
+
404
+ false
405
+ end
406
+
407
+ # Check if a path needs runtime resolution
408
+ #
409
+ # @param path [String] the path to check
410
+ # @return [Boolean] true if path needs runtime resolution
411
+ #
412
+ def path_needs_runtime_resolution?(path)
413
+ # Paths with array indices may need runtime resolution
414
+ path =~ /\.\d+\./ || path.start_with?(".")
415
+ end
416
+
417
+ # Find paths similar to the given path
418
+ #
419
+ # @param path [String] the path to match
420
+ # @return [Array<String>] similar paths
421
+ #
422
+ def find_similar_paths(path)
423
+ Utils::StringDistance.similar_strings(path, @path_index.keys, max_distance: 3, limit: 3)
424
+ end
425
+
426
+ # Check if field is a copy() type
427
+ #
428
+ # @param field [Field] the field to check
429
+ # @return [Boolean] true if copy type
430
+ #
431
+ def copy_type?(field)
432
+ field.type_name == "copy"
433
+ end
434
+
435
+ # Extract the path argument from a copy() type
436
+ #
437
+ # @param field [Field] the copy field
438
+ # @return [String, nil] the path or nil
439
+ #
440
+ def extract_copy_path(field)
441
+ args = field.type_args
442
+ args[:path] || args["path"] || args.values.first
443
+ end
444
+
445
+ # Check if field is a nested schema type
446
+ #
447
+ # @param field [Field] the field to check
448
+ # @return [Boolean] true if nested schema
449
+ #
450
+ def nested_schema?(field)
451
+ type_name = field.type_name
452
+ return false unless type_name
453
+
454
+ # Not a builtin type and exists in registry
455
+ !Types::Registry.registered?(type_name) && schema_exists?(type_name)
456
+ end
457
+
458
+ # Check if field is a map_by_field type
459
+ #
460
+ # @param field [Field] the field to check
461
+ # @return [Boolean] true if map_by_field
462
+ #
463
+ def map_by_field?(field)
464
+ field.type_name == "map_by_field"
465
+ end
466
+
467
+ # Check if field is a Ref() type
468
+ #
469
+ # @param field [Field] the field to check
470
+ # @return [Boolean] true if Ref type
471
+ #
472
+ def ref_type?(field)
473
+ field.type_name == "reference" || field.type_name == "Ref"
474
+ end
475
+
476
+ # Check if field is an array with nested schema
477
+ #
478
+ # @param field [Field] the field to check
479
+ # @return [Boolean] true if array with schema element
480
+ #
481
+ def array_with_nested_schema?(field)
482
+ return false unless field.type_name == "array"
483
+
484
+ element_type = field.type_args[:element]
485
+ return false unless element_type
486
+
487
+ schema_exists?(element_type.to_s)
488
+ end
489
+
490
+ # Check if a schema exists (without mutex deadlock)
491
+ #
492
+ # @param name [String] schema name
493
+ # @return [Boolean] true if schema exists
494
+ #
495
+ def schema_exists?(name)
496
+ @schemas_cache.key?(name.to_s)
497
+ end
498
+
499
+ # Get a schema (without mutex deadlock)
500
+ #
501
+ # @param name [String] schema name
502
+ # @return [Schema] the schema
503
+ #
504
+ def get_schema(name)
505
+ @schemas_cache[name.to_s]
506
+ end
507
+
508
+ # Detect circular references in schema definitions
509
+ #
510
+ # @return [Array<CycleError>] list of cycle errors
511
+ #
512
+ def detect_cycles
513
+ errors = []
514
+ visited = {}
515
+ rec_stack = {}
516
+
517
+ @schemas_cache.each_key do |schema_name|
518
+ cycle = find_cycle(schema_name, visited, rec_stack, [])
519
+ errors << Errors::CycleError.new(cycle) if cycle
520
+ end
521
+
522
+ errors
523
+ end
524
+
525
+ # Find a cycle starting from the given schema
526
+ #
527
+ # @param name [String] schema name to start from
528
+ # @param visited [Hash] globally visited schemas
529
+ # @param rec_stack [Hash] current recursion stack
530
+ # @param path [Array<String>] current path through schemas
531
+ # @return [Array<String>, nil] cycle path if found, nil otherwise
532
+ #
533
+ def find_cycle(name, visited, rec_stack, path)
534
+ # Skip if already fully processed
535
+ return nil if visited[name]
536
+
537
+ # If we're back in the recursion stack, we found a cycle
538
+ if rec_stack[name]
539
+ cycle_start = path.index(name)
540
+ return path[cycle_start..] + [name]
541
+ end
542
+
543
+ # Skip if schema doesn't exist
544
+ return nil unless schema_exists?(name)
545
+
546
+ # Mark as being processed
547
+ rec_stack[name] = true
548
+ current_path = path + [name]
549
+
550
+ schema = get_schema(name)
551
+ return nil unless schema
552
+
553
+ # Check all referenced schemas
554
+ schema.fields.each do |field|
555
+ referenced_schemas = get_referenced_schemas(field)
556
+
557
+ referenced_schemas.each do |ref_name|
558
+ cycle = find_cycle(ref_name, visited, rec_stack, current_path)
559
+ return cycle if cycle
560
+ end
561
+ end
562
+
563
+ # Mark as fully processed
564
+ rec_stack[name] = false
565
+ visited[name] = true
566
+ nil
567
+ end
568
+
569
+ # Get all schemas referenced by a field
570
+ #
571
+ # @param field [Field] the field to check
572
+ # @return [Array<String>] list of referenced schema names
573
+ #
574
+ def get_referenced_schemas(field)
575
+ refs = []
576
+
577
+ # Direct schema reference
578
+ if nested_schema?(field)
579
+ refs << field.type_name
580
+ end
581
+
582
+ # Array element type
583
+ if array_with_nested_schema?(field)
584
+ element_type = field.type_args[:element]
585
+ refs << element_type.to_s if element_type
586
+ end
587
+
588
+ # map_by_field entries
589
+ if map_by_field?(field)
590
+ entries = field.type_args[:entries] || {}
591
+ entries.each do |_, entry_info|
592
+ schema_name = if entry_info.is_a?(Hash)
593
+ entry_info[:schema] || entry_info["schema"]
594
+ else
595
+ entry_info.to_s
596
+ end
597
+ refs << schema_name if schema_name
598
+ end
599
+ end
600
+
601
+ # one_of schemas
602
+ if field.type_name == "one_of"
603
+ schemas = field.type_args[:schemas] || []
604
+ schemas.each do |s|
605
+ schema_name = s.is_a?(Hash) ? (s[:schema] || s["schema"]) : s.to_s
606
+ refs << schema_name if schema_name
607
+ end
608
+ end
609
+
610
+ refs.select { |r| schema_exists?(r) }
611
+ end
612
+ end
613
+ end
614
+ end
615
+
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Version
6
+ # =============================================================================
7
+ #
8
+ # This file defines the version constant for the Synthra gem.
9
+ # The version follows Semantic Versioning (https://semver.org/):
10
+ #
11
+ # MAJOR.MINOR.PATCH
12
+ # - MAJOR: Incremented for incompatible API changes
13
+ # - MINOR: Incremented for backward-compatible functionality additions
14
+ # - PATCH: Incremented for backward-compatible bug fixes
15
+ #
16
+ # =============================================================================
17
+
18
+
19
+ # :nocov:
20
+ module Synthra
21
+
22
+ # Current version of the Synthra gem
23
+ #
24
+ # @return [String] the semantic version string
25
+ #
26
+ # @example Check version
27
+ # puts Synthra::VERSION # => "0.1.0"
28
+ #
29
+ # @example Compare versions
30
+ # Gem::Version.new(Synthra::VERSION) >= Gem::Version.new("0.1.0")
31
+ #
32
+
33
+ VERSION = "0.1.0"
34
+ end
35
+ # :nocov: