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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +40 -0
- data/.yardopts +18 -0
- data/ACCESS_DOCS.md +116 -0
- data/ADVANCED_FEATURES_SUMMARY.md +245 -0
- data/CHANGELOG.md +498 -0
- data/CODE_OF_CONDUCT.md +122 -0
- data/CONTRIBUTING.md +307 -0
- data/DOCUMENTATION.md +204 -0
- data/DOCUMENTATION_GUIDE.md +194 -0
- data/LICENSE +22 -0
- data/README.md +1458 -0
- data/Rakefile +128 -0
- data/benchmark/README.md +591 -0
- data/benchmark/batch_generation.rb +63 -0
- data/benchmark/engine_comparison.rb +295 -0
- data/benchmark/single_record.rb +108 -0
- data/benchmark/streaming.rb +74 -0
- data/docs/ACTIVERECORD_INFERENCE.md +524 -0
- data/docs/API_SERVER.md +278 -0
- data/docs/CONFIG_FILE.md +315 -0
- data/docs/DATA_CONTRACTS.md +312 -0
- data/docs/ENHANCED_REPL.md +304 -0
- data/docs/FACTORY_BOT.md +271 -0
- data/docs/GITHUB_ACTION.md +420 -0
- data/docs/GRAPHQL_EXPORT.md +303 -0
- data/docs/GRAPHQL_FEDERATION.md +651 -0
- data/docs/HOW_TO_GENERATE_DOCS.md +252 -0
- data/docs/LSP.md +189 -0
- data/docs/MIGRATION_GENERATOR.md +547 -0
- data/docs/MOCK_SERVER.md +280 -0
- data/docs/NATIVE_ENGINE.md +298 -0
- data/docs/OPENAPI_EXPORT.md +252 -0
- data/docs/PERFORMANCE_MODE.md +228 -0
- data/docs/PERSONAS.md +547 -0
- data/docs/PROPERTY_TESTING.md +284 -0
- data/docs/PROTOBUF_EXPORT.md +276 -0
- data/docs/QUALITY_METRICS.md +555 -0
- data/docs/QUICK_REFERENCE.md +88 -0
- data/docs/RAILS_ENGINE.md +404 -0
- data/docs/RAILS_INTEGRATION.md +454 -0
- data/docs/README.md +175 -0
- data/docs/REPL_QUICK_REFERENCE.md +149 -0
- data/docs/RUST_INTEGRATION_GUIDE.md +1376 -0
- data/docs/SCENARIOS.md +576 -0
- data/docs/SECURITY_TESTING.md +318 -0
- data/docs/SNAPSHOT_TESTING.md +308 -0
- data/docs/STRING_TEXT_TYPES_REFERENCE.md +512 -0
- data/docs/TERRAFORM_EXPORT.md +447 -0
- data/docs/TIME_TRAVEL.md +507 -0
- data/docs/USAGE_GUIDE.md +980 -0
- data/docs/WEBHOOK_SIMULATOR.md +579 -0
- data/docs/advanced_features.md +183 -0
- data/docs/api_reference.md +153 -0
- data/docs/behaviors.md +138 -0
- data/docs/best_practices.md +191 -0
- data/docs/dsl_reference.md +172 -0
- data/docs/examples.md +162 -0
- data/docs/generation_modes.md +125 -0
- data/docs/getting_started.md +92 -0
- data/docs/index.html +177 -0
- data/docs/strategic/CLOUD_REGISTRY_ARCHITECTURE.md +268 -0
- data/docs/strategic/SIMD_OPTIMIZATION_STRATEGY.md +153 -0
- data/docs/troubleshooting.md +215 -0
- data/docs/type_reference.md +224 -0
- data/examples/hostile_mode_demo.rb +64 -0
- data/exe/synthra +6 -0
- data/ext/synthra_native/Cargo.lock +972 -0
- data/ext/synthra_native/Cargo.toml +46 -0
- data/ext/synthra_native/extconf.rb +41 -0
- data/ext/synthra_native/src/generator.rs +397 -0
- data/ext/synthra_native/src/lib.rs +229 -0
- data/ext/synthra_native/src/types.rs +1485 -0
- data/lib/generators/synthra/install_generator.rb +86 -0
- data/lib/generators/synthra/templates/api_response.dsl +15 -0
- data/lib/generators/synthra/templates/fake_data.rake +116 -0
- data/lib/generators/synthra/templates/synthra.yml +31 -0
- data/lib/generators/synthra/templates/synthra_support.rb +19 -0
- data/lib/generators/synthra/templates/user.dsl +11 -0
- data/lib/synthra/activerecord_inference.rb +394 -0
- data/lib/synthra/api.rb +381 -0
- data/lib/synthra/api_server.rb +718 -0
- data/lib/synthra/behaviors/applicator.rb +165 -0
- data/lib/synthra/behaviors/base.rb +155 -0
- data/lib/synthra/behaviors/close_connection.rb +60 -0
- data/lib/synthra/behaviors/deprecated.rb +100 -0
- data/lib/synthra/behaviors/failure.rb +69 -0
- data/lib/synthra/behaviors/latency.rb +103 -0
- data/lib/synthra/behaviors/partial_data.rb +83 -0
- data/lib/synthra/behaviors/randomize_order.rb +74 -0
- data/lib/synthra/behaviors/registry.rb +238 -0
- data/lib/synthra/behaviors/simulate_error.rb +79 -0
- data/lib/synthra/cli/commands/base.rb +105 -0
- data/lib/synthra/cli/commands/diff.rb +227 -0
- data/lib/synthra/cli/commands/docs.rb +67 -0
- data/lib/synthra/cli/commands/export.rb +150 -0
- data/lib/synthra/cli/commands/generate.rb +72 -0
- data/lib/synthra/cli/commands/import.rb +80 -0
- data/lib/synthra/cli/commands/lint.rb +53 -0
- data/lib/synthra/cli/commands/live.rb +57 -0
- data/lib/synthra/cli/commands/seed.rb +99 -0
- data/lib/synthra/cli/commands/validate.rb +28 -0
- data/lib/synthra/cli.rb +2471 -0
- data/lib/synthra/config_file.rb +129 -0
- data/lib/synthra/configuration.rb +281 -0
- data/lib/synthra/contracts_registry.rb +408 -0
- data/lib/synthra/database_seeder.rb +268 -0
- data/lib/synthra/deterministic_ids.rb +218 -0
- data/lib/synthra/documentation_generator.rb +414 -0
- data/lib/synthra/engine.rb +251 -0
- data/lib/synthra/errors.rb +1169 -0
- data/lib/synthra/export/base.rb +85 -0
- data/lib/synthra/export/csv.rb +101 -0
- data/lib/synthra/export/graphql.rb +266 -0
- data/lib/synthra/export/graphql_federation.rb +377 -0
- data/lib/synthra/export/graphviz.rb +258 -0
- data/lib/synthra/export/javascript.rb +327 -0
- data/lib/synthra/export/json_data.rb +61 -0
- data/lib/synthra/export/json_schema.rb +290 -0
- data/lib/synthra/export/openapi.rb +514 -0
- data/lib/synthra/export/protobuf.rb +483 -0
- data/lib/synthra/export/python.rb +560 -0
- data/lib/synthra/export/sql.rb +381 -0
- data/lib/synthra/export/sql_insert.rb +152 -0
- data/lib/synthra/export/terraform.rb +501 -0
- data/lib/synthra/export/type_mapping.rb +389 -0
- data/lib/synthra/export/typescript.rb +300 -0
- data/lib/synthra/export/xml_data.rb +104 -0
- data/lib/synthra/export/yaml_data.rb +56 -0
- data/lib/synthra/export.rb +404 -0
- data/lib/synthra/factory_bot_integration.rb +157 -0
- data/lib/synthra/field.rb +440 -0
- data/lib/synthra/functions/registry.rb +104 -0
- data/lib/synthra/generator/context.rb +372 -0
- data/lib/synthra/generator/engine.rb +336 -0
- data/lib/synthra/generator/faker_adapter.rb +425 -0
- data/lib/synthra/generator/modes.rb +444 -0
- data/lib/synthra/generator/resolver.rb +256 -0
- data/lib/synthra/generator/rng.rb +273 -0
- data/lib/synthra/generator/streamer.rb +63 -0
- data/lib/synthra/generator/uniqueness.rb +118 -0
- data/lib/synthra/initializer.rb +97 -0
- data/lib/synthra/limits.rb +279 -0
- data/lib/synthra/live_preview.rb +518 -0
- data/lib/synthra/loader_config.rb +78 -0
- data/lib/synthra/lsp/server.rb +689 -0
- data/lib/synthra/migration_generator.rb +301 -0
- data/lib/synthra/mixin.rb +174 -0
- data/lib/synthra/mock_server.rb +458 -0
- data/lib/synthra/native_engine.rb +304 -0
- data/lib/synthra/openapi_importer.rb +228 -0
- data/lib/synthra/output/json_formatter.rb +40 -0
- data/lib/synthra/output/ndjson_formatter.rb +55 -0
- data/lib/synthra/parser/ast.rb +1287 -0
- data/lib/synthra/parser/lexer.rb +1152 -0
- data/lib/synthra/parser/parser.rb +1664 -0
- data/lib/synthra/parser/tokens.rb +461 -0
- data/lib/synthra/performance_mode.rb +364 -0
- data/lib/synthra/personas.rb +397 -0
- data/lib/synthra/property_testing.rb +172 -0
- data/lib/synthra/quality_metrics.rb +405 -0
- data/lib/synthra/rails_test_helper.rb +248 -0
- data/lib/synthra/registry.rb +533 -0
- data/lib/synthra/relationships.rb +193 -0
- data/lib/synthra/repl/enhanced_repl.rb +607 -0
- data/lib/synthra/repl/formatter.rb +191 -0
- data/lib/synthra/scenarios.rb +423 -0
- data/lib/synthra/schema.rb +605 -0
- data/lib/synthra/schema_inheritance.rb +104 -0
- data/lib/synthra/schema_versioning.rb +212 -0
- data/lib/synthra/snapshot_testing.rb +199 -0
- data/lib/synthra/time_travel.rb +338 -0
- data/lib/synthra/type_definitions.rb +274 -0
- data/lib/synthra/types/address_location/addresses.rb +125 -0
- data/lib/synthra/types/address_location/airports.rb +200 -0
- data/lib/synthra/types/address_location/banks_hospitals.rb +207 -0
- data/lib/synthra/types/address_location/locations.rb +406 -0
- data/lib/synthra/types/base.rb +48 -0
- data/lib/synthra/types/commerce_products/commerce.rb +119 -0
- data/lib/synthra/types/commerce_products/companies.rb +71 -0
- data/lib/synthra/types/commerce_products/construction.rb +126 -0
- data/lib/synthra/types/commerce_products/products.rb +205 -0
- data/lib/synthra/types/core/collections.rb +513 -0
- data/lib/synthra/types/core/defaults.rb +76 -0
- data/lib/synthra/types/core/enums.rb +102 -0
- data/lib/synthra/types/core/identifiers.rb +445 -0
- data/lib/synthra/types/core/primitives.rb +586 -0
- data/lib/synthra/types/core/references.rb +466 -0
- data/lib/synthra/types/core/sequences.rb +304 -0
- data/lib/synthra/types/crypto/crypto.rb +162 -0
- data/lib/synthra/types/date_time/dates.rb +387 -0
- data/lib/synthra/types/finance_banking/banking.rb +424 -0
- data/lib/synthra/types/finance_banking/credit_cards.rb +24 -0
- data/lib/synthra/types/finance_banking/identifiers.rb +93 -0
- data/lib/synthra/types/formula.rb +184 -0
- data/lib/synthra/types/health_medical/medical.rb +150 -0
- data/lib/synthra/types/hostile_payloads.rb +122 -0
- data/lib/synthra/types/json_array.rb +51 -0
- data/lib/synthra/types/media_entertainment/media.rb +62 -0
- data/lib/synthra/types/naughty_string.rb +30 -0
- data/lib/synthra/types/personal_names/chinese.rb +41 -0
- data/lib/synthra/types/personal_names/identifiers.rb +187 -0
- data/lib/synthra/types/personal_names/names.rb +246 -0
- data/lib/synthra/types/personal_names/national_id.rb +89 -0
- data/lib/synthra/types/personal_names/titles_suffixes.rb +41 -0
- data/lib/synthra/types/regex.rb +248 -0
- data/lib/synthra/types/registry.rb +182 -0
- data/lib/synthra/types/repeating_element.rb +50 -0
- data/lib/synthra/types/scenario.rb +29 -0
- data/lib/synthra/types/technology_internet/apps.rb +67 -0
- data/lib/synthra/types/technology_internet/communication.rb +162 -0
- data/lib/synthra/types/technology_internet/devices.rb +80 -0
- data/lib/synthra/types/technology_internet/formats.rb +139 -0
- data/lib/synthra/types/technology_internet/networking.rb +143 -0
- data/lib/synthra/types/template.rb +92 -0
- data/lib/synthra/types/text_content/business.rb +128 -0
- data/lib/synthra/types/text_content/colors.rb +70 -0
- data/lib/synthra/types/text_content/misc.rb +237 -0
- data/lib/synthra/types/text_content/security.rb +130 -0
- data/lib/synthra/types/text_content/text_generation.rb +532 -0
- data/lib/synthra/types/travel/travel.rb +150 -0
- data/lib/synthra/utils/string_distance.rb +133 -0
- data/lib/synthra/validator/dsl_validator.rb +339 -0
- data/lib/synthra/validator/path_validator.rb +615 -0
- data/lib/synthra/version.rb +35 -0
- data/lib/synthra/webhook_simulator.rb +341 -0
- data/lib/synthra.rb +259 -0
- data/schemas/address.dsl +16 -0
- data/schemas/api_response.dsl +8 -0
- data/schemas/error_payload.dsl +9 -0
- data/schemas/order.dsl +10 -0
- data/schemas/order_item.dsl +8 -0
- data/schemas/payment.dsl +11 -0
- data/schemas/social_post.dsl +13 -0
- data/schemas/user.dsl +10 -0
- data/scripts/batch_fix_all.rb +96 -0
- data/scripts/delete_old_files.rb +32 -0
- data/scripts/fix_all_grouped_files.rb +137 -0
- data/scripts/fix_all_indentation.rb +48 -0
- data/scripts/fix_all_syntax.rb +124 -0
- data/scripts/fix_grouped_files.rb +184 -0
- data/scripts/fix_syntax_errors.rb +108 -0
- data/scripts/group_domain_types.rb +112 -0
- data/scripts/group_domain_types_fixed.rb +150 -0
- data/scripts/merge_domains_to_one_file.rb +68 -0
- data/scripts/move_existing_types.rb +130 -0
- data/scripts/split_grouped_types.rb +142 -0
- data/tech_docs/README.md +134 -0
- data/tech_docs/advanced/streaming.md +405 -0
- data/tech_docs/advanced/thread_safety.md +253 -0
- data/tech_docs/api/overview.md +485 -0
- data/tech_docs/appendices/type_chart.md +193 -0
- data/tech_docs/basic_concepts.md +383 -0
- data/tech_docs/behaviors/overview.md +346 -0
- data/tech_docs/dsl/complex_types.md +790 -0
- data/tech_docs/dsl/core_types.md +464 -0
- data/tech_docs/dsl/datetime_types.md +325 -0
- data/tech_docs/dsl/field_modifiers.md +414 -0
- data/tech_docs/dsl/grammar.md +431 -0
- data/tech_docs/dsl/schema_definition.md +399 -0
- data/tech_docs/export/README.md +276 -0
- data/tech_docs/installation.md +273 -0
- data/tech_docs/integration/ci_cd.md +707 -0
- data/tech_docs/integration/ci_cd_guide.md +579 -0
- data/tech_docs/integration/factory_bot.md +485 -0
- data/tech_docs/integration/rails.md +630 -0
- data/tech_docs/integration/rspec.md +449 -0
- data/tech_docs/modes/overview.md +350 -0
- data/tech_docs/performance/NATIVE_RUST_EXTENSION.md +1270 -0
- data/tech_docs/performance/OPTIMIZATION_GUIDE.md +901 -0
- data/tech_docs/quick_start.md +256 -0
- data/tech_docs/templates/README.md +805 -0
- data/tech_docs/tutorials/advanced.md +371 -0
- data/tech_docs/tutorials/getting_started.md +189 -0
- data/tech_docs/tutorials/intermediate.md +231 -0
- data/tech_docs/tutorials/template_gallery.md +569 -0
- data/tech_example/01_basic_usage.rb +238 -0
- data/tech_example/02_types_demo.rb +336 -0
- data/tech_example/04_cli_usage.md +429 -0
- data/tech_example/05_database_seeding.rb +283 -0
- data/tech_example/07_rspec_integration.rb +359 -0
- data/tech_example/10_custom_types.rb +387 -0
- data/tech_example/12_behaviors.rb +372 -0
- data/tech_example/13_twitter_dm_example.rb +120 -0
- data/tech_example/14_exact_json_structure.rb +253 -0
- data/tech_example/16_lsp_server.rb +56 -0
- data/tech_example/17_property_testing.rb +92 -0
- data/tech_example/18_enhanced_repl.rb +120 -0
- data/tech_example/19_security_fuzzing.rb +145 -0
- data/tech_example/NEW_FEATURES.md +169 -0
- data/tech_example/README.md +118 -0
- data/tech_example/schemas/api_response.dsl +123 -0
- data/tech_example/schemas/ecommerce.dsl +121 -0
- data/tech_example/schemas/twitter_dm.dsl +109 -0
- data/tech_example/schemas/user.dsl +36 -0
- data/vscode-extension/README.md +246 -0
- data/vscode-extension/language-configuration.json +31 -0
- data/vscode-extension/package.json +55 -0
- data/vscode-extension/snippets/fakedatadsl.json +198 -0
- data/vscode-extension/syntaxes/fakedatadsl.tmLanguage.json +128 -0
- 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
|