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,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Data Quality Metrics
5
+ #
6
+ # Analyze generated data for quality, realism, and distribution.
7
+ # Useful for validating test data quality and debugging generation issues.
8
+ #
9
+ # @example Analyze generated data
10
+ # records = Synthra.generate_many("User", 1000)
11
+ # report = Synthra::QualityMetrics.analyze(records, schema: "User")
12
+ #
13
+ # report.uniqueness[:email] # => 100.0
14
+ # report.distribution[:role] # => { "admin" => 5, "user" => 95 }
15
+ # report.realism_score # => 94.5
16
+ #
17
+ module QualityMetrics
18
+ class << self
19
+ # Analyze a collection of records
20
+ #
21
+ # @param records [Array<Hash>] generated records
22
+ # @param schema [String, Schema] schema for context
23
+ # @return [QualityReport] analysis report
24
+ #
25
+ def analyze(records, schema: nil)
26
+ schema = Synthra.registry.schema(schema.to_s) if schema.is_a?(String)
27
+
28
+ analyzer = Analyzer.new(records, schema)
29
+ analyzer.analyze
30
+ end
31
+
32
+ # Generate data with quality report
33
+ #
34
+ # @param schema_name [String] schema name
35
+ # @param count [Integer] number of records
36
+ # @param options [Hash] generation options
37
+ # @return [GenerationResult] records with quality report
38
+ #
39
+ def generate_with_quality(schema_name, count: 100, **options)
40
+ schema = Synthra.registry.schema(schema_name)
41
+ records = schema.generate_many(count, **options)
42
+ report = analyze(records, schema: schema)
43
+
44
+ GenerationResult.new(records, report)
45
+ end
46
+ end
47
+
48
+ # Data quality analyzer
49
+ class Analyzer
50
+ def initialize(records, schema = nil)
51
+ @records = records
52
+ @schema = schema
53
+ @field_names = detect_field_names
54
+ end
55
+
56
+ def analyze
57
+ QualityReport.new(
58
+ total_records: @records.length,
59
+ uniqueness: calculate_uniqueness,
60
+ distribution: calculate_distribution,
61
+ completeness: calculate_completeness,
62
+ type_consistency: calculate_type_consistency,
63
+ realism_score: calculate_realism_score,
64
+ patterns: detect_patterns,
65
+ anomalies: detect_anomalies
66
+ )
67
+ end
68
+
69
+ private
70
+
71
+ def detect_field_names
72
+ return @schema.fields.map(&:name) if @schema
73
+
74
+ @records.first&.keys || []
75
+ end
76
+
77
+ def calculate_uniqueness
78
+ result = {}
79
+
80
+ @field_names.each do |field|
81
+ values = @records.map { |r| r[field] }.compact
82
+ next if values.empty?
83
+
84
+ unique_count = values.uniq.length
85
+ total_count = values.length
86
+ result[field] = (unique_count.to_f / total_count * 100).round(2)
87
+ end
88
+
89
+ result
90
+ end
91
+
92
+ def calculate_distribution
93
+ result = {}
94
+
95
+ @field_names.each do |field|
96
+ values = @records.map { |r| r[field] }.compact
97
+
98
+ # Only calculate distribution for fields with limited cardinality
99
+ unique_values = values.uniq
100
+ next if unique_values.length > 50 || unique_values.length == values.length
101
+
102
+ distribution = values.tally.transform_values do |count|
103
+ (count.to_f / values.length * 100).round(2)
104
+ end
105
+
106
+ result[field] = distribution
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ def calculate_completeness
113
+ total_fields = @records.length * @field_names.length
114
+ return 100.0 if total_fields.zero?
115
+
116
+ filled_fields = @records.sum do |record|
117
+ @field_names.count { |f| !record[f].nil? && record[f] != "" }
118
+ end
119
+
120
+ (filled_fields.to_f / total_fields * 100).round(2)
121
+ end
122
+
123
+ def calculate_type_consistency
124
+ result = {}
125
+
126
+ @field_names.each do |field|
127
+ values = @records.map { |r| r[field] }.compact
128
+ next if values.empty?
129
+
130
+ types = values.map { |v| classify_type(v) }
131
+ type_counts = types.tally
132
+
133
+ # Consistency = % of most common type
134
+ most_common_count = type_counts.values.max
135
+ result[field] = (most_common_count.to_f / types.length * 100).round(2)
136
+ end
137
+
138
+ result
139
+ end
140
+
141
+ def calculate_realism_score
142
+ scores = []
143
+
144
+ # Score based on uniqueness of ID fields
145
+ id_fields = @field_names.select { |f| f.match?(/\A(id|uuid|_id)\z/i) }
146
+ id_fields.each do |field|
147
+ uniqueness = calculate_uniqueness[field] || 0
148
+ scores << uniqueness
149
+ end
150
+
151
+ # Score based on email format
152
+ email_fields = @field_names.select { |f| f.match?(/email/i) }
153
+ email_fields.each do |field|
154
+ values = @records.map { |r| r[field] }.compact
155
+ valid_emails = values.count { |v| v.to_s.match?(/@.*\./) }
156
+ scores << (valid_emails.to_f / values.length * 100) if values.any?
157
+ end
158
+
159
+ # Score based on completeness
160
+ scores << calculate_completeness
161
+
162
+ # Score based on type consistency
163
+ type_scores = calculate_type_consistency.values
164
+ scores << (type_scores.sum / type_scores.length) if type_scores.any?
165
+
166
+ # :nocov: unreachable in practice — completeness above always appends one score, so `scores`
167
+ # is never empty; kept as a defensive guard against future refactors.
168
+ return 0.0 if scores.empty?
169
+ # :nocov:
170
+
171
+ (scores.sum / scores.length).round(2)
172
+ end
173
+
174
+ def detect_patterns
175
+ patterns = {}
176
+
177
+ @field_names.each do |field|
178
+ values = @records.map { |r| r[field] }.compact
179
+ next if values.empty?
180
+
181
+ string_values = values.select { |v| v.is_a?(String) }
182
+ next if string_values.length < 10
183
+
184
+ # Detect length patterns
185
+ lengths = string_values.map(&:length)
186
+ patterns[field] = {
187
+ min_length: lengths.min,
188
+ max_length: lengths.max,
189
+ avg_length: (lengths.sum.to_f / lengths.length).round(2),
190
+ common_prefixes: find_common_prefixes(string_values),
191
+ common_suffixes: find_common_suffixes(string_values)
192
+ }
193
+ end
194
+
195
+ patterns
196
+ end
197
+
198
+ def detect_anomalies
199
+ anomalies = []
200
+
201
+ # Check for unexpected null ratios
202
+ @field_names.each do |field|
203
+ values = @records.map { |r| r[field] }
204
+ null_ratio = values.count(&:nil?).to_f / values.length
205
+
206
+ if null_ratio > 0.5
207
+ anomalies << {
208
+ type: :high_null_ratio,
209
+ field: field,
210
+ ratio: (null_ratio * 100).round(2)
211
+ }
212
+ end
213
+ end
214
+
215
+ # Check for duplicate IDs
216
+ id_fields = @field_names.select { |f| f.match?(/\A(id|uuid|_id)\z/i) }
217
+ id_fields.each do |field|
218
+ values = @records.map { |r| r[field] }.compact
219
+ duplicates = values.length - values.uniq.length
220
+
221
+ if duplicates > 0
222
+ anomalies << {
223
+ type: :duplicate_ids,
224
+ field: field,
225
+ count: duplicates
226
+ }
227
+ end
228
+ end
229
+
230
+ # Check for unrealistic distributions
231
+ calculate_distribution.each do |field, dist|
232
+ max_percentage = dist.values.max || 0
233
+ if max_percentage > 90
234
+ anomalies << {
235
+ type: :skewed_distribution,
236
+ field: field,
237
+ dominant_value: dist.key(max_percentage),
238
+ percentage: max_percentage
239
+ }
240
+ end
241
+ end
242
+
243
+ anomalies
244
+ end
245
+
246
+ def classify_type(value)
247
+ case value
248
+ when nil then :null
249
+ when Integer then :integer
250
+ when Float then :float
251
+ when TrueClass, FalseClass then :boolean
252
+ when Array then :array
253
+ when Hash then :object
254
+ when String
255
+ if value.match?(/\A\d{4}-\d{2}-\d{2}/)
256
+ :datetime
257
+ elsif value.match?(/\A[0-9a-f-]{36}\z/i)
258
+ :uuid
259
+ else
260
+ :string
261
+ end
262
+ else
263
+ :unknown
264
+ end
265
+ end
266
+
267
+ def find_common_prefixes(strings, min_length: 2)
268
+ return [] if strings.length < 10
269
+
270
+ prefixes = Hash.new(0)
271
+ strings.each do |s|
272
+ (min_length..[s.length, 10].min).each do |len|
273
+ prefixes[s[0, len]] += 1
274
+ end
275
+ end
276
+
277
+ prefixes.select { |_, count| count > strings.length * 0.1 }
278
+ .sort_by { |_, count| -count }
279
+ .first(3)
280
+ .map(&:first)
281
+ end
282
+
283
+ def find_common_suffixes(strings, min_length: 2)
284
+ return [] if strings.length < 10
285
+
286
+ suffixes = Hash.new(0)
287
+ strings.each do |s|
288
+ (min_length..[s.length, 10].min).each do |len|
289
+ suffixes[s[-len..]] += 1
290
+ end
291
+ end
292
+
293
+ suffixes.select { |_, count| count > strings.length * 0.1 }
294
+ .sort_by { |_, count| -count }
295
+ .first(3)
296
+ .map(&:first)
297
+ end
298
+ end
299
+
300
+ # Quality analysis report
301
+ class QualityReport
302
+ attr_reader :total_records, :uniqueness, :distribution, :completeness
303
+ attr_reader :type_consistency, :realism_score, :patterns, :anomalies
304
+
305
+ def initialize(**attributes)
306
+ @total_records = attributes[:total_records]
307
+ @uniqueness = attributes[:uniqueness]
308
+ @distribution = attributes[:distribution]
309
+ @completeness = attributes[:completeness]
310
+ @type_consistency = attributes[:type_consistency]
311
+ @realism_score = attributes[:realism_score]
312
+ @patterns = attributes[:patterns]
313
+ @anomalies = attributes[:anomalies]
314
+ end
315
+
316
+ # Check if data passes quality threshold
317
+ #
318
+ # @param threshold [Float] minimum realism score (0-100)
319
+ # @return [Boolean]
320
+ #
321
+ def passes?(threshold: 80)
322
+ realism_score >= threshold && anomalies.empty?
323
+ end
324
+
325
+ # Get a summary string
326
+ #
327
+ # @return [String]
328
+ #
329
+ def summary
330
+ lines = [
331
+ "Quality Report (#{total_records} records)",
332
+ "=" * 40,
333
+ "Realism Score: #{realism_score}%",
334
+ "Completeness: #{completeness}%",
335
+ "",
336
+ "Uniqueness:",
337
+ *uniqueness.map { |f, u| " #{f}: #{u}%" },
338
+ ""
339
+ ]
340
+
341
+ if anomalies.any?
342
+ lines << "Anomalies:"
343
+ anomalies.each do |a|
344
+ lines << " ⚠️ #{a[:type]} in #{a[:field]}"
345
+ end
346
+ else
347
+ lines << "✅ No anomalies detected"
348
+ end
349
+
350
+ lines.join("\n")
351
+ end
352
+
353
+ # Convert to hash
354
+ def to_h
355
+ {
356
+ total_records: total_records,
357
+ uniqueness: uniqueness,
358
+ distribution: distribution,
359
+ completeness: completeness,
360
+ type_consistency: type_consistency,
361
+ realism_score: realism_score,
362
+ patterns: patterns,
363
+ anomalies: anomalies
364
+ }
365
+ end
366
+
367
+ def to_json(*args)
368
+ to_h.to_json(*args)
369
+ end
370
+ end
371
+
372
+ # Result with both records and quality report
373
+ class GenerationResult
374
+ attr_reader :records, :quality_report
375
+
376
+ def initialize(records, quality_report)
377
+ @records = records
378
+ @quality_report = quality_report
379
+ end
380
+
381
+ # Delegate array methods to records
382
+ def each(&block)
383
+ @records.each(&block)
384
+ end
385
+
386
+ def map(&block)
387
+ @records.map(&block)
388
+ end
389
+
390
+ def length
391
+ @records.length
392
+ end
393
+
394
+ alias size length
395
+
396
+ def [](index)
397
+ @records[index]
398
+ end
399
+
400
+ def to_a
401
+ @records
402
+ end
403
+ end
404
+ end
405
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Rails Test Helper Integration
5
+ #
6
+ # Provides seamless integration with Rails tests, allowing you to generate
7
+ # test data directly from ActiveRecord models or Synthra schemas.
8
+ #
9
+ # @example Setup in test_helper.rb
10
+ # require 'synthra/rails_test_helper'
11
+ # Synthra.configure_test_helper!
12
+ #
13
+ # class ActiveSupport::TestCase
14
+ # include Synthra::RailsTestHelper
15
+ # end
16
+ #
17
+ # @example Usage in tests
18
+ # class UserTest < ActiveSupport::TestCase
19
+ # test "validates email format" do
20
+ # user = generate_for_test(User)
21
+ # assert user.valid?
22
+ # end
23
+ # end
24
+ #
25
+ module RailsTestHelper
26
+ # Generate test data for a model or schema
27
+ #
28
+ # @param model_or_schema [Class, String, Schema] ActiveRecord model, schema name, or Schema
29
+ # @param mode [Symbol] generation mode (:valid, :invalid, :edge, :random)
30
+ # @param traits [Array<Symbol>] traits to apply
31
+ # @param overrides [Hash] field overrides
32
+ # @return [Object] generated model instance or hash
33
+ #
34
+ # @example Generate valid user
35
+ # user = generate_for_test(User)
36
+ #
37
+ # @example Generate with mode
38
+ # user = generate_for_test(User, :invalid)
39
+ #
40
+ # @example With overrides
41
+ # user = generate_for_test(User, :valid, email: "test@example.com")
42
+ #
43
+ def generate_for_test(model_or_schema, mode = :valid, **overrides)
44
+ schema = resolve_schema(model_or_schema)
45
+ generation_mode = mode == :valid ? :random : mode
46
+
47
+ data = schema.generate(mode: generation_mode, overrides: overrides)
48
+
49
+ if model_or_schema.is_a?(Class) && defined?(ActiveRecord) && model_or_schema < ActiveRecord::Base
50
+ model_or_schema.new(filter_attributes(model_or_schema, data))
51
+ else
52
+ data
53
+ end
54
+ end
55
+
56
+ # Generate and persist test data
57
+ #
58
+ # @param model [Class] ActiveRecord model class
59
+ # @param mode [Symbol] generation mode
60
+ # @param overrides [Hash] field overrides
61
+ # @return [Object] persisted model instance
62
+ #
63
+ # @example Create user in database
64
+ # user = create_for_test(User)
65
+ # assert user.persisted?
66
+ #
67
+ def create_for_test(model, mode = :valid, **overrides)
68
+ record = generate_for_test(model, mode, **overrides)
69
+ record.save! if record.respond_to?(:save!)
70
+ record
71
+ end
72
+
73
+ # Generate multiple test records
74
+ #
75
+ # @param model_or_schema [Class, String, Schema] model or schema
76
+ # @param count [Integer] number of records
77
+ # @param mode [Symbol] generation mode
78
+ # @param overrides [Hash] field overrides
79
+ # @return [Array] array of generated records
80
+ #
81
+ # @example Generate 10 users
82
+ # users = generate_many_for_test(User, 10)
83
+ #
84
+ def generate_many_for_test(model_or_schema, count, mode = :valid, **overrides)
85
+ count.times.map { generate_for_test(model_or_schema, mode, **overrides) }
86
+ end
87
+
88
+ # Generate and persist multiple test records
89
+ #
90
+ # @param model [Class] ActiveRecord model
91
+ # @param count [Integer] number of records
92
+ # @param mode [Symbol] generation mode
93
+ # @param overrides [Hash] field overrides
94
+ # @return [Array] array of persisted records
95
+ #
96
+ def create_many_for_test(model, count, mode = :valid, **overrides)
97
+ count.times.map { create_for_test(model, mode, **overrides) }
98
+ end
99
+
100
+ private
101
+
102
+ def resolve_schema(model_or_schema)
103
+ case model_or_schema
104
+ when Synthra::Schema
105
+ model_or_schema
106
+ when String, Symbol
107
+ Synthra.parse(Synthra::SchemaInference.infer_dsl(model_or_schema.to_s))
108
+ when Class
109
+ if defined?(ActiveRecord) && model_or_schema < ActiveRecord::Base
110
+ Synthra.parse(Synthra::SchemaInference.from_active_record(model_or_schema))
111
+ else
112
+ Synthra.parse(Synthra::SchemaInference.infer_dsl(model_or_schema.name))
113
+ end
114
+ else
115
+ raise ArgumentError, "Unknown model/schema type: #{model_or_schema.class}"
116
+ end
117
+ end
118
+
119
+ def filter_attributes(model, data)
120
+ return data unless defined?(ActiveRecord) && model < ActiveRecord::Base
121
+
122
+ allowed = model.column_names
123
+ data.select { |k, _| allowed.include?(k.to_s) }
124
+ end
125
+ end
126
+
127
+ # Schema Inference from ActiveRecord models
128
+ #
129
+ # Automatically generates Synthra schemas from ActiveRecord models
130
+ # by inspecting column types, validations, and associations.
131
+ #
132
+ module SchemaInference
133
+ class << self
134
+ # Generate DSL from ActiveRecord model
135
+ #
136
+ # @param model [Class] ActiveRecord model class
137
+ # @return [String] DSL string
138
+ #
139
+ def from_active_record(model)
140
+ lines = ["#{model.name}:"]
141
+
142
+ model.columns.each do |column|
143
+ next if skip_column?(column.name)
144
+
145
+ type = infer_type_from_column(column, model)
146
+ optional = column.null ? "?" : ""
147
+ lines << " #{column.name}#{optional}: #{type}"
148
+ end
149
+
150
+ lines.join("\n")
151
+ end
152
+
153
+ # Generate minimal DSL from schema name
154
+ #
155
+ # @param name [String] schema name
156
+ # @return [String] DSL string
157
+ #
158
+ def infer_dsl(name)
159
+ <<~DSL
160
+ #{name}:
161
+ id: uuid
162
+ DSL
163
+ end
164
+
165
+ private
166
+
167
+ def skip_column?(name)
168
+ %w[id created_at updated_at].include?(name.to_s)
169
+ end
170
+
171
+ def infer_type_from_column(column, model)
172
+ # Check for specific patterns in column name
173
+ name = column.name.to_s
174
+
175
+ return "email" if name.include?("email")
176
+ return "phone" if name.include?("phone")
177
+ return "url" if name.include?("url") || name.include?("link")
178
+ return "uuid" if name.end_with?("_id") || name == "uuid"
179
+ return "password" if name.include?("password")
180
+ return "name" if name == "name" || name.end_with?("_name")
181
+ return "address" if name.include?("address")
182
+ return "city" if name == "city"
183
+ return "state" if name == "state"
184
+ return "country" if name == "country"
185
+ return "postal_code" if name.include?("zip") || name.include?("postal")
186
+
187
+ # Fall back to column type
188
+ case column.type
189
+ when :string, :text
190
+ infer_string_type(column, model)
191
+ when :integer, :bigint
192
+ "number"
193
+ when :float, :decimal
194
+ "money"
195
+ when :boolean
196
+ "boolean"
197
+ when :date
198
+ "date"
199
+ when :datetime, :timestamp
200
+ "timestamp"
201
+ when :uuid
202
+ "uuid"
203
+ when :json, :jsonb
204
+ "object"
205
+ else
206
+ "text"
207
+ end
208
+ end
209
+
210
+ def infer_string_type(column, model)
211
+ # Check for enum
212
+ if model.respond_to?(:defined_enums) && model.defined_enums.key?(column.name.to_s)
213
+ values = model.defined_enums[column.name.to_s].keys
214
+ return "enum(#{values.join(', ')})"
215
+ end
216
+
217
+ # Check for length constraint
218
+ if column.limit && column.limit <= 50
219
+ "text(1..#{column.limit})"
220
+ else
221
+ "text"
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ class << self
228
+ # Configure Rails test helper
229
+ #
230
+ # @return [void]
231
+ #
232
+ def configure_test_helper!
233
+ return unless defined?(Rails)
234
+
235
+ # Auto-load schemas from standard locations
236
+ schema_paths = [
237
+ Rails.root.join("schemas"),
238
+ Rails.root.join("test/schemas"),
239
+ Rails.root.join("spec/schemas")
240
+ ]
241
+
242
+ @default_registry ||= Registry.new
243
+ schema_paths.each do |path|
244
+ @default_registry.load_dir(path.to_s) if path.exist?
245
+ end
246
+ end
247
+ end
248
+ end