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,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Schema Versioning Support
|
|
5
|
+
#
|
|
6
|
+
# Track schema versions and migration paths for API evolution.
|
|
7
|
+
#
|
|
8
|
+
# @example DSL syntax
|
|
9
|
+
# User@v1:
|
|
10
|
+
# id: uuid
|
|
11
|
+
# name: name
|
|
12
|
+
#
|
|
13
|
+
# User@v2:
|
|
14
|
+
# @migrated_from User@v1
|
|
15
|
+
# id: uuid
|
|
16
|
+
# first_name: name
|
|
17
|
+
# last_name: name
|
|
18
|
+
#
|
|
19
|
+
module SchemaVersioning
|
|
20
|
+
# Parse version from schema name
|
|
21
|
+
#
|
|
22
|
+
# @param name [String] schema name with optional version
|
|
23
|
+
# @return [Hash] { name: String, version: String|nil }
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# parse_version("User@v2")
|
|
27
|
+
# # => { name: "User", version: "v2" }
|
|
28
|
+
#
|
|
29
|
+
def self.parse_version(name)
|
|
30
|
+
if name.include?("@")
|
|
31
|
+
parts = name.split("@", 2)
|
|
32
|
+
{ name: parts[0], version: parts[1] }
|
|
33
|
+
else
|
|
34
|
+
{ name: name, version: nil }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Generate schema key for registry
|
|
39
|
+
#
|
|
40
|
+
# @param name [String] schema name
|
|
41
|
+
# @param version [String, nil] version
|
|
42
|
+
# @return [String] registry key
|
|
43
|
+
#
|
|
44
|
+
def self.registry_key(name, version = nil)
|
|
45
|
+
version ? "#{name}@#{version}" : name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Version registry for tracking migrations
|
|
49
|
+
class Registry
|
|
50
|
+
class << self
|
|
51
|
+
def instance
|
|
52
|
+
@instance ||= new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def register(from:, to:, changes: {})
|
|
56
|
+
instance.register(from: from, to: to, changes: changes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_migration(from, to)
|
|
60
|
+
instance.get_migration(from, to)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def migration_path(from, to)
|
|
64
|
+
instance.migration_path(from, to)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def versions_for(schema_name)
|
|
68
|
+
instance.versions_for(schema_name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def clear
|
|
72
|
+
instance.clear
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize
|
|
77
|
+
@migrations = {}
|
|
78
|
+
@versions = Hash.new { |h, k| h[k] = [] }
|
|
79
|
+
@mutex = Mutex.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Register a migration from one version to another
|
|
83
|
+
#
|
|
84
|
+
# @param from [String] source version (e.g., "User@v1")
|
|
85
|
+
# @param to [String] target version (e.g., "User@v2")
|
|
86
|
+
# @param changes [Hash] migration changes
|
|
87
|
+
#
|
|
88
|
+
def register(from:, to:, changes: {})
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@migrations[[from, to]] = changes
|
|
91
|
+
|
|
92
|
+
# Track versions
|
|
93
|
+
from_parsed = SchemaVersioning.parse_version(from)
|
|
94
|
+
to_parsed = SchemaVersioning.parse_version(to)
|
|
95
|
+
|
|
96
|
+
@versions[from_parsed[:name]] << from_parsed[:version]
|
|
97
|
+
@versions[to_parsed[:name]] << to_parsed[:version]
|
|
98
|
+
@versions[from_parsed[:name]].uniq!
|
|
99
|
+
@versions[to_parsed[:name]].uniq!
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get migration between two versions
|
|
104
|
+
#
|
|
105
|
+
# @param from [String] source version
|
|
106
|
+
# @param to [String] target version
|
|
107
|
+
# @return [Hash, nil] migration info
|
|
108
|
+
#
|
|
109
|
+
def get_migration(from, to)
|
|
110
|
+
@mutex.synchronize { @migrations[[from, to]] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get all versions for a schema
|
|
114
|
+
#
|
|
115
|
+
# @param schema_name [String] schema name without version
|
|
116
|
+
# @return [Array<String>] versions
|
|
117
|
+
#
|
|
118
|
+
def versions_for(schema_name)
|
|
119
|
+
@mutex.synchronize { @versions[schema_name].sort }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Find migration path between versions
|
|
123
|
+
#
|
|
124
|
+
# @param from [String] source version
|
|
125
|
+
# @param to [String] target version
|
|
126
|
+
# @return [Array<Array>] path of migrations
|
|
127
|
+
#
|
|
128
|
+
def migration_path(from, to)
|
|
129
|
+
@mutex.synchronize do
|
|
130
|
+
# Simple BFS to find path
|
|
131
|
+
visited = Set.new
|
|
132
|
+
queue = [[from, []]]
|
|
133
|
+
|
|
134
|
+
while queue.any?
|
|
135
|
+
current, path = queue.shift
|
|
136
|
+
return path if current == to
|
|
137
|
+
next if visited.include?(current)
|
|
138
|
+
|
|
139
|
+
visited.add(current)
|
|
140
|
+
|
|
141
|
+
@migrations.keys.each do |from_key, to_key|
|
|
142
|
+
if from_key == current && !visited.include?(to_key)
|
|
143
|
+
queue << [to_key, path + [[from_key, to_key]]]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
nil # No path found
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def clear
|
|
153
|
+
@mutex.synchronize do
|
|
154
|
+
@migrations.clear
|
|
155
|
+
@versions.clear
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Migration analyzer
|
|
161
|
+
class Analyzer
|
|
162
|
+
# Detect breaking changes between two schemas
|
|
163
|
+
#
|
|
164
|
+
# @param old_schema [Schema] old version
|
|
165
|
+
# @param new_schema [Schema] new version
|
|
166
|
+
# @return [Hash] analysis results
|
|
167
|
+
#
|
|
168
|
+
def self.analyze(old_schema, new_schema)
|
|
169
|
+
old_fields = old_schema.fields.map(&:name)
|
|
170
|
+
new_fields = new_schema.fields.map(&:name)
|
|
171
|
+
|
|
172
|
+
removed_fields = old_fields - new_fields
|
|
173
|
+
added_fields = new_fields - old_fields
|
|
174
|
+
common_fields = old_fields & new_fields
|
|
175
|
+
|
|
176
|
+
type_changes = []
|
|
177
|
+
common_fields.each do |name|
|
|
178
|
+
old_field = old_schema.field(name)
|
|
179
|
+
new_field = new_schema.field(name)
|
|
180
|
+
|
|
181
|
+
if old_field.type_name != new_field.type_name
|
|
182
|
+
type_changes << {
|
|
183
|
+
field: name,
|
|
184
|
+
from: old_field.type_name,
|
|
185
|
+
to: new_field.type_name
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
breaking: removed_fields.any? || type_changes.any?,
|
|
192
|
+
removed_fields: removed_fields,
|
|
193
|
+
added_fields: added_fields,
|
|
194
|
+
type_changes: type_changes,
|
|
195
|
+
summary: generate_summary(removed_fields, added_fields, type_changes)
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def self.generate_summary(removed, added, type_changes)
|
|
202
|
+
parts = []
|
|
203
|
+
parts << "Removed: #{removed.join(', ')}" if removed.any?
|
|
204
|
+
parts << "Added: #{added.join(', ')}" if added.any?
|
|
205
|
+
type_changes.each do |change|
|
|
206
|
+
parts << "Changed #{change[:field]}: #{change[:from]} → #{change[:to]}"
|
|
207
|
+
end
|
|
208
|
+
parts.join("\n")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Snapshot Testing Support
|
|
5
|
+
#
|
|
6
|
+
# Provides helpers for snapshot testing generated data.
|
|
7
|
+
# Ensures schema changes are detected and intentional.
|
|
8
|
+
#
|
|
9
|
+
# @example RSpec integration
|
|
10
|
+
# RSpec.describe "Schema Snapshots" do
|
|
11
|
+
# include Synthra::SnapshotTesting
|
|
12
|
+
#
|
|
13
|
+
# Synthra.each_schema do |schema|
|
|
14
|
+
# it "#{schema.name} generates stable output" do
|
|
15
|
+
# expect_snapshot(schema, seed: 42)
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
module SnapshotTesting
|
|
21
|
+
# Default snapshot directory
|
|
22
|
+
SNAPSHOT_DIR = "spec/snapshots/synthra"
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Snapshot directory path
|
|
26
|
+
attr_accessor :snapshot_dir
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
self.snapshot_dir = SNAPSHOT_DIR
|
|
30
|
+
|
|
31
|
+
# Generate and compare snapshot for a schema
|
|
32
|
+
#
|
|
33
|
+
# @param schema [Schema] the schema to snapshot
|
|
34
|
+
# @param seed [Integer] seed for deterministic generation
|
|
35
|
+
# @param update [Boolean] if true, update the snapshot file
|
|
36
|
+
# @return [Boolean] true if snapshot matches
|
|
37
|
+
#
|
|
38
|
+
def expect_snapshot(schema, seed: 42, update: false)
|
|
39
|
+
snapshot_path = snapshot_path_for(schema)
|
|
40
|
+
generated = schema.generate(seed: seed)
|
|
41
|
+
generated_json = JSON.pretty_generate(sort_hash(generated))
|
|
42
|
+
|
|
43
|
+
if update || !File.exist?(snapshot_path)
|
|
44
|
+
write_snapshot(snapshot_path, generated_json)
|
|
45
|
+
return true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
expected = File.read(snapshot_path)
|
|
49
|
+
|
|
50
|
+
if generated_json == expected
|
|
51
|
+
true
|
|
52
|
+
else
|
|
53
|
+
report_snapshot_mismatch(schema.name, expected, generated_json)
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# RSpec matcher for snapshot testing
|
|
59
|
+
#
|
|
60
|
+
# @param schema_name [String] schema name for snapshot file
|
|
61
|
+
# @return [SnapshotMatcher]
|
|
62
|
+
#
|
|
63
|
+
def match_snapshot(schema_name)
|
|
64
|
+
SnapshotMatcher.new(schema_name, snapshot_dir: SnapshotTesting.snapshot_dir)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Generate snapshots for all schemas
|
|
68
|
+
#
|
|
69
|
+
# @param registry [Registry] schema registry
|
|
70
|
+
# @param seed [Integer] seed for deterministic generation
|
|
71
|
+
#
|
|
72
|
+
def self.generate_all_snapshots(registry, seed: 42)
|
|
73
|
+
FileUtils.mkdir_p(snapshot_dir)
|
|
74
|
+
|
|
75
|
+
registry.schemas.each do |name, schema|
|
|
76
|
+
path = File.join(snapshot_dir, "#{name.downcase}.json")
|
|
77
|
+
generated = schema.generate(seed: seed)
|
|
78
|
+
File.write(path, JSON.pretty_generate(deep_sort(generated)))
|
|
79
|
+
puts "Generated snapshot: #{path}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Verify all snapshots match
|
|
84
|
+
#
|
|
85
|
+
# @param registry [Registry] schema registry
|
|
86
|
+
# @param seed [Integer] seed for deterministic generation
|
|
87
|
+
# @return [Array<String>] list of mismatched schema names
|
|
88
|
+
#
|
|
89
|
+
def self.verify_all_snapshots(registry, seed: 42)
|
|
90
|
+
mismatches = []
|
|
91
|
+
|
|
92
|
+
registry.schemas.each do |name, schema|
|
|
93
|
+
path = File.join(snapshot_dir, "#{name.downcase}.json")
|
|
94
|
+
|
|
95
|
+
unless File.exist?(path)
|
|
96
|
+
mismatches << "#{name}: snapshot not found"
|
|
97
|
+
next
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
generated = schema.generate(seed: seed)
|
|
101
|
+
expected = JSON.parse(File.read(path))
|
|
102
|
+
|
|
103
|
+
unless deep_sort(generated) == deep_sort(expected)
|
|
104
|
+
mismatches << name
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
mismatches
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Deep sort helper for module methods
|
|
112
|
+
def self.deep_sort(obj)
|
|
113
|
+
case obj
|
|
114
|
+
when Hash
|
|
115
|
+
obj.sort.to_h.transform_values { |v| deep_sort(v) }
|
|
116
|
+
when Array
|
|
117
|
+
obj.map { |v| deep_sort(v) }
|
|
118
|
+
else
|
|
119
|
+
obj
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def snapshot_path_for(schema)
|
|
126
|
+
dir = SnapshotTesting.snapshot_dir
|
|
127
|
+
FileUtils.mkdir_p(dir)
|
|
128
|
+
File.join(dir, "#{schema.name.downcase}.json")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def write_snapshot(path, content)
|
|
132
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
133
|
+
File.write(path, content)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def sort_hash(obj)
|
|
137
|
+
case obj
|
|
138
|
+
when Hash
|
|
139
|
+
obj.sort.to_h.transform_values { |v| sort_hash(v) }
|
|
140
|
+
when Array
|
|
141
|
+
obj.map { |v| sort_hash(v) }
|
|
142
|
+
else
|
|
143
|
+
obj
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def report_snapshot_mismatch(name, expected, actual)
|
|
148
|
+
puts "\nSnapshot mismatch for #{name}:"
|
|
149
|
+
puts "Expected:"
|
|
150
|
+
puts expected.lines.first(10).join
|
|
151
|
+
puts "..."
|
|
152
|
+
puts "\nActual:"
|
|
153
|
+
puts actual.lines.first(10).join
|
|
154
|
+
puts "..."
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# RSpec matcher class
|
|
158
|
+
class SnapshotMatcher
|
|
159
|
+
def initialize(schema_name, snapshot_dir:)
|
|
160
|
+
@schema_name = schema_name
|
|
161
|
+
@snapshot_dir = snapshot_dir
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def matches?(actual)
|
|
165
|
+
@actual = actual
|
|
166
|
+
path = File.join(@snapshot_dir, "#{@schema_name.downcase}.json")
|
|
167
|
+
|
|
168
|
+
return false unless File.exist?(path)
|
|
169
|
+
|
|
170
|
+
@expected = JSON.parse(File.read(path))
|
|
171
|
+
sorted_actual = sort_hash(@actual)
|
|
172
|
+
sorted_expected = sort_hash(@expected)
|
|
173
|
+
|
|
174
|
+
sorted_actual == sorted_expected
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def failure_message
|
|
178
|
+
"expected generated data to match snapshot for #{@schema_name}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def failure_message_when_negated
|
|
182
|
+
"expected generated data not to match snapshot for #{@schema_name}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def sort_hash(obj)
|
|
188
|
+
case obj
|
|
189
|
+
when Hash
|
|
190
|
+
obj.sort.to_h.transform_values { |v| sort_hash(v) }
|
|
191
|
+
when Array
|
|
192
|
+
obj.map { |v| sort_hash(v) }
|
|
193
|
+
else
|
|
194
|
+
obj
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|