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,444 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Generator
|
|
5
|
+
|
|
6
|
+
# Generation modes for different types of fake data
|
|
7
|
+
module Modes
|
|
8
|
+
|
|
9
|
+
# Curated edge case strings
|
|
10
|
+
|
|
11
|
+
EDGE_STRINGS = [
|
|
12
|
+
"", # empty
|
|
13
|
+
" ", # single space
|
|
14
|
+
" ", # multiple spaces
|
|
15
|
+
"\t\n\r", # whitespace chars
|
|
16
|
+
"🎉💀🔥👻", # emojis
|
|
17
|
+
"مرحبا", # RTL Arabic
|
|
18
|
+
"日本語", # Japanese
|
|
19
|
+
"SELECT * FROM users;", # SQL injection attempt
|
|
20
|
+
'{"key": "value"}', # JSON in string
|
|
21
|
+
"<script>alert(1)</script>", # XSS attempt
|
|
22
|
+
"a" * 1000, # very long
|
|
23
|
+
"NULL", # string null
|
|
24
|
+
"undefined", # string undefined
|
|
25
|
+
"true", # string boolean
|
|
26
|
+
"123", # string number
|
|
27
|
+
"\x00\x00", # null bytes
|
|
28
|
+
"O'Brien", # apostrophe
|
|
29
|
+
"a\nb\nc" # newlines
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Curated edge case numbers
|
|
34
|
+
|
|
35
|
+
EDGE_NUMBERS = [
|
|
36
|
+
0,
|
|
37
|
+
-1,
|
|
38
|
+
1,
|
|
39
|
+
-2_147_483_648, # INT_MIN
|
|
40
|
+
2_147_483_647, # INT_MAX
|
|
41
|
+
9_223_372_036_854_775_807, # BIGINT_MAX
|
|
42
|
+
Float::MIN,
|
|
43
|
+
Float::MAX
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Curated edge case dates
|
|
48
|
+
|
|
49
|
+
EDGE_DATES = [
|
|
50
|
+
Date.new(1970, 1, 1), # Unix epoch
|
|
51
|
+
Date.new(2000, 2, 29), # Leap day
|
|
52
|
+
Date.new(2038, 1, 19), # Y2038
|
|
53
|
+
Date.new(2020, 1, 1), # Fixed date for determinism (replaces Date.today)
|
|
54
|
+
Date.new(1, 1, 1), # Very old
|
|
55
|
+
Date.new(9999, 12, 31) # Far future
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Base mode class
|
|
60
|
+
#
|
|
61
|
+
# Abstract base class for all generation modes. Each mode implements
|
|
62
|
+
# the #generate method to produce values in its specific style.
|
|
63
|
+
#
|
|
64
|
+
# @abstract Subclass and implement {#generate}
|
|
65
|
+
#
|
|
66
|
+
|
|
67
|
+
class BaseMode
|
|
68
|
+
|
|
69
|
+
# Create a new mode instance
|
|
70
|
+
#
|
|
71
|
+
# @param rng [Generator::RNG] random number generator
|
|
72
|
+
#
|
|
73
|
+
|
|
74
|
+
def initialize(rng)
|
|
75
|
+
@rng = rng
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Generate a value using this mode
|
|
80
|
+
#
|
|
81
|
+
# @abstract Must be implemented by subclasses
|
|
82
|
+
# @param _type [Base] type generator instance
|
|
83
|
+
# @param _args [Hash] type arguments
|
|
84
|
+
# @return [Object] generated value
|
|
85
|
+
# @raise [NotImplementedError] if not overridden
|
|
86
|
+
#
|
|
87
|
+
|
|
88
|
+
def generate(_type, _args)
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Random mode - standard Faker-based generation
|
|
95
|
+
#
|
|
96
|
+
# Generates realistic fake data using Faker library. This is the
|
|
97
|
+
# default mode for normal data generation.
|
|
98
|
+
#
|
|
99
|
+
|
|
100
|
+
class RandomMode < BaseMode
|
|
101
|
+
|
|
102
|
+
# Generate a random value
|
|
103
|
+
#
|
|
104
|
+
# @param type_instance [Base] type generator instance
|
|
105
|
+
# @param args [Hash] type arguments
|
|
106
|
+
# @return [Object] generated value
|
|
107
|
+
#
|
|
108
|
+
|
|
109
|
+
def generate(type_instance, args, context = nil)
|
|
110
|
+
context ||= Generator::Context.new
|
|
111
|
+
type_instance.generate_random(@rng, context, args)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Edge mode - curated edge cases
|
|
117
|
+
#
|
|
118
|
+
# Generates edge case values (empty strings, boundary values, nulls)
|
|
119
|
+
# useful for testing boundary conditions and error handling.
|
|
120
|
+
#
|
|
121
|
+
|
|
122
|
+
class EdgeMode < BaseMode
|
|
123
|
+
|
|
124
|
+
# Generate an edge case value
|
|
125
|
+
#
|
|
126
|
+
# @param type_instance [Base] type generator instance
|
|
127
|
+
# @param args [Hash] type arguments
|
|
128
|
+
# @return [Object] edge case value
|
|
129
|
+
#
|
|
130
|
+
|
|
131
|
+
def generate(type_instance, args, context = nil)
|
|
132
|
+
context ||= Generator::Context.new
|
|
133
|
+
type_instance.generate_edge(@rng, context, args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Get an edge case string
|
|
138
|
+
#
|
|
139
|
+
# Returns a curated edge case string from EDGE_STRINGS.
|
|
140
|
+
#
|
|
141
|
+
# @return [String] edge case string
|
|
142
|
+
#
|
|
143
|
+
|
|
144
|
+
def edge_string
|
|
145
|
+
EDGE_STRINGS.sample(random: @rng.instance_variable_get(:@random))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Get an edge case number
|
|
150
|
+
#
|
|
151
|
+
# Returns a curated edge case number from EDGE_NUMBERS, optionally
|
|
152
|
+
# filtered by range.
|
|
153
|
+
#
|
|
154
|
+
# @param range [Range, nil] optional range to filter candidates
|
|
155
|
+
# @return [Numeric] edge case number
|
|
156
|
+
#
|
|
157
|
+
|
|
158
|
+
def edge_number(range = nil)
|
|
159
|
+
candidates = EDGE_NUMBERS
|
|
160
|
+
if range
|
|
161
|
+
candidates = candidates.select { |n| range.cover?(n) }
|
|
162
|
+
end
|
|
163
|
+
candidates.empty? ? 0 : candidates.sample(random: @rng.instance_variable_get(:@random))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Get an edge case date
|
|
168
|
+
#
|
|
169
|
+
# Returns a curated edge case date from EDGE_DATES.
|
|
170
|
+
#
|
|
171
|
+
# @return [Date] edge case date
|
|
172
|
+
#
|
|
173
|
+
|
|
174
|
+
def edge_date
|
|
175
|
+
EDGE_DATES.sample(random: @rng.instance_variable_get(:@random))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Invalid mode - intentionally wrong data
|
|
181
|
+
#
|
|
182
|
+
# Generates intentionally invalid values (wrong types, malformed data)
|
|
183
|
+
# useful for testing validation logic.
|
|
184
|
+
#
|
|
185
|
+
|
|
186
|
+
class InvalidMode < BaseMode
|
|
187
|
+
|
|
188
|
+
# Generate an invalid value
|
|
189
|
+
#
|
|
190
|
+
# @param type_instance [Base] type generator instance
|
|
191
|
+
# @param args [Hash] type arguments
|
|
192
|
+
# @return [Object] invalid value
|
|
193
|
+
#
|
|
194
|
+
|
|
195
|
+
def generate(type_instance, args, context = nil)
|
|
196
|
+
context ||= Generator::Context.new
|
|
197
|
+
type_instance.generate_invalid(@rng, context, args)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Get an invalid string value
|
|
202
|
+
#
|
|
203
|
+
# Returns a value that is not a valid string.
|
|
204
|
+
#
|
|
205
|
+
# @return [Object] invalid value (nil, number, array, hash, boolean)
|
|
206
|
+
#
|
|
207
|
+
|
|
208
|
+
def invalid_string
|
|
209
|
+
@rng.sample([nil, 123, [], {}, true])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Get an invalid number value
|
|
214
|
+
#
|
|
215
|
+
# Returns a value that is not a valid number.
|
|
216
|
+
#
|
|
217
|
+
# @return [Object] invalid value (string, nil, array)
|
|
218
|
+
#
|
|
219
|
+
|
|
220
|
+
def invalid_number
|
|
221
|
+
@rng.sample(["not a number", nil, "NaN", "Infinity", "", []])
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Get an invalid email value
|
|
226
|
+
#
|
|
227
|
+
# Returns a value that is not a valid email address.
|
|
228
|
+
#
|
|
229
|
+
# @return [Object] invalid value (malformed email, nil, number)
|
|
230
|
+
#
|
|
231
|
+
|
|
232
|
+
def invalid_email
|
|
233
|
+
@rng.sample([
|
|
234
|
+
"notanemail",
|
|
235
|
+
"@missing.local",
|
|
236
|
+
"missing@",
|
|
237
|
+
"double@@at.com",
|
|
238
|
+
"spaces in@email.com",
|
|
239
|
+
nil,
|
|
240
|
+
123
|
|
241
|
+
])
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Get an invalid UUID value
|
|
246
|
+
#
|
|
247
|
+
# Returns a value that is not a valid UUID.
|
|
248
|
+
#
|
|
249
|
+
# @return [Object] invalid value (string, nil, number)
|
|
250
|
+
#
|
|
251
|
+
|
|
252
|
+
def invalid_uuid
|
|
253
|
+
@rng.sample([
|
|
254
|
+
"not-a-uuid",
|
|
255
|
+
"12345",
|
|
256
|
+
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
257
|
+
nil,
|
|
258
|
+
123
|
|
259
|
+
])
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Mixed mode - combination of random, edge, and invalid
|
|
265
|
+
#
|
|
266
|
+
# Probabilistically mixes all three modes according to specified ratios.
|
|
267
|
+
# Default: 80% random, 15% edge, 5% invalid.
|
|
268
|
+
#
|
|
269
|
+
|
|
270
|
+
class MixedMode < BaseMode
|
|
271
|
+
|
|
272
|
+
# Default mode ratios
|
|
273
|
+
# @return [Hash] ratio percentages
|
|
274
|
+
DEFAULT_RATIOS = {
|
|
275
|
+
random: 80,
|
|
276
|
+
edge: 15,
|
|
277
|
+
invalid: 5
|
|
278
|
+
}.freeze
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Create a new MixedMode instance
|
|
282
|
+
#
|
|
283
|
+
# @param rng [Generator::RNG] random number generator
|
|
284
|
+
# @param ratios [Hash] mode ratios (default: DEFAULT_RATIOS)
|
|
285
|
+
#
|
|
286
|
+
|
|
287
|
+
def initialize(rng, ratios: DEFAULT_RATIOS)
|
|
288
|
+
super(rng)
|
|
289
|
+
@ratios = ratios
|
|
290
|
+
@random_mode = RandomMode.new(rng)
|
|
291
|
+
@edge_mode = EdgeMode.new(rng)
|
|
292
|
+
@invalid_mode = InvalidMode.new(rng)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Generate a value using mixed mode
|
|
297
|
+
#
|
|
298
|
+
# Selects a mode probabilistically based on ratios and generates
|
|
299
|
+
# a value using that mode.
|
|
300
|
+
#
|
|
301
|
+
# @param type_instance [Base] type generator instance
|
|
302
|
+
# @param args [Hash] type arguments
|
|
303
|
+
# @return [Object] generated value
|
|
304
|
+
#
|
|
305
|
+
|
|
306
|
+
def generate(type_instance, args, context = nil)
|
|
307
|
+
context ||= Generator::Context.new
|
|
308
|
+
roll = @rng.rand(100)
|
|
309
|
+
cumulative = 0
|
|
310
|
+
|
|
311
|
+
if roll < (cumulative += @ratios[:random])
|
|
312
|
+
@random_mode.generate(type_instance, args, context)
|
|
313
|
+
elsif roll < (cumulative += @ratios[:edge])
|
|
314
|
+
@edge_mode.generate(type_instance, args, context)
|
|
315
|
+
else
|
|
316
|
+
@invalid_mode.generate(type_instance, args, context)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# Hostile mode - security attack payloads
|
|
323
|
+
#
|
|
324
|
+
# Generates security attack payloads (SQL injection, XSS, buffer overflow)
|
|
325
|
+
# for security testing of API endpoints consuming this data.
|
|
326
|
+
#
|
|
327
|
+
class HostileMode < BaseMode
|
|
328
|
+
# Security attack payloads
|
|
329
|
+
SQL_INJECTION_PAYLOADS = [
|
|
330
|
+
"' OR '1'='1",
|
|
331
|
+
"'; DROP TABLE users;--",
|
|
332
|
+
"' UNION SELECT * FROM users--",
|
|
333
|
+
"1' OR '1'='1",
|
|
334
|
+
"admin'--",
|
|
335
|
+
"' OR 1=1;--",
|
|
336
|
+
"1' AND '1'='1",
|
|
337
|
+
"' OR 'x'='x",
|
|
338
|
+
"') OR ('1'='1--",
|
|
339
|
+
"1' OR '1'='1' /*"
|
|
340
|
+
].freeze
|
|
341
|
+
|
|
342
|
+
XSS_PAYLOADS = [
|
|
343
|
+
"<script>alert(1)</script>",
|
|
344
|
+
"<img src=x onerror=alert(1)>",
|
|
345
|
+
"<svg onload=alert(1)>",
|
|
346
|
+
"javascript:alert(1)",
|
|
347
|
+
"<body onload=alert(1)>",
|
|
348
|
+
"<iframe src=javascript:alert(1)>",
|
|
349
|
+
"<input onfocus=alert(1) autofocus>",
|
|
350
|
+
"<select onfocus=alert(1) autofocus>",
|
|
351
|
+
"<textarea onfocus=alert(1) autofocus>",
|
|
352
|
+
"<keygen onfocus=alert(1) autofocus>"
|
|
353
|
+
].freeze
|
|
354
|
+
|
|
355
|
+
BUFFER_OVERFLOW_PAYLOADS = [
|
|
356
|
+
"A" * 1000,
|
|
357
|
+
"A" * 10000,
|
|
358
|
+
"A" * 100000,
|
|
359
|
+
"\x00" * 1000,
|
|
360
|
+
"\xFF" * 1000
|
|
361
|
+
].freeze
|
|
362
|
+
|
|
363
|
+
UNICODE_ATTACKS = [
|
|
364
|
+
"\u202E" + "evil" + "\u202D", # RTL override
|
|
365
|
+
"\uFEFF" + "text", # BOM
|
|
366
|
+
"\u200B" * 100, # Zero-width spaces
|
|
367
|
+
"\u2060" * 100, # Word joiner
|
|
368
|
+
"Zalgo: H\u0301\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u0309\u030A\u030B\u030C\u030D\u030E\u030F\u0310\u0311\u0312\u0313\u0314\u0315\u0316\u0317\u0318\u0319\u031A\u031B\u031C\u031D\u031E\u031F\u0320\u0321\u0322\u0323\u0324\u0325\u0326\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F\u0330\u0331\u0332\u0333\u0334\u0335\u0336\u0337\u0338\u0339\u033A\u033B\u033C\u033D\u033E\u033F\u0340\u0341\u0342\u0343\u0344\u0345\u0346\u0347\u0348\u0349\u034A\u034B\u034C\u034D\u034E\u034F\u0350\u0351\u0352\u0353\u0354\u0355\u0356\u0357\u0358\u0359\u035A\u035B\u035C\u035D\u035E\u035F\u0360\u0361\u0362\u0363\u0364\u0365\u0366\u0367\u0368\u0369\u036A\u036B\u036C\u036D\u036E\u036F\u0489\u048A\u048B\u048C\u048D\u048E\u048F\u0490\u0491\u0492\u0493\u0494\u0495\u0496\u0497\u0498\u0499\u049A\u049B\u049C\u049D\u049E\u049F\u04A0\u04A1\u04A2\u04A3\u04A4\u04A5\u04A6\u04A7\u04A8\u04A9\u04AA\u04AB\u04AC\u04AD\u04AE\u04AF\u04B0\u04B1\u04B2\u04B3\u04B4\u04B5\u04B6\u04B7\u04B8\u04B9\u04BA\u04BB\u04BC\u04BD\u04BE\u04BF\u04C0\u04C1\u04C2\u04C3\u04C4\u04C5\u04C6\u04C7\u04C8\u04C9\u04CA\u04CB\u04CC\u04CD\u04CE\u04CF\u04D0\u04D1\u04D2\u04D3\u04D4\u04D5\u04D6\u04D7\u04D8\u04D9\u04DA\u04DB\u04DC\u04DD\u04DE\u04DF\u04E0\u04E1\u04E2\u04E3\u04E4\u04E5\u04E6\u04E7\u04E8\u04E9\u04EA\u04EB\u04EC\u04ED\u04EE\u04EF\u04F0\u04F1\u04F2\u04F3\u04F4\u04F5\u04F6\u04F7\u04F8\u04F9\u04FA\u04FB\u04FC\u04FD\u04FE\u04FF"
|
|
369
|
+
].freeze
|
|
370
|
+
|
|
371
|
+
# Generate a hostile value
|
|
372
|
+
#
|
|
373
|
+
# @param type_instance [Base] type generator instance
|
|
374
|
+
# @param args [Hash] type arguments
|
|
375
|
+
# @return [Object] hostile value
|
|
376
|
+
#
|
|
377
|
+
def generate(type_instance, args, context = nil)
|
|
378
|
+
context ||= Generator::Context.new
|
|
379
|
+
type_instance.generate_hostile(@rng, context, args)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Get SQL injection payload
|
|
383
|
+
#
|
|
384
|
+
# @return [String] SQL injection string
|
|
385
|
+
#
|
|
386
|
+
def sql_injection
|
|
387
|
+
SQL_INJECTION_PAYLOADS.sample(random: @rng.instance_variable_get(:@random))
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Get XSS payload
|
|
391
|
+
#
|
|
392
|
+
# @return [String] XSS attack string
|
|
393
|
+
#
|
|
394
|
+
def xss_payload
|
|
395
|
+
XSS_PAYLOADS.sample(random: @rng.instance_variable_get(:@random))
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Get buffer overflow payload
|
|
399
|
+
#
|
|
400
|
+
# @return [String] buffer overflow string
|
|
401
|
+
#
|
|
402
|
+
def buffer_overflow
|
|
403
|
+
BUFFER_OVERFLOW_PAYLOADS.sample(random: @rng.instance_variable_get(:@random))
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Get Unicode attack payload
|
|
407
|
+
#
|
|
408
|
+
# @return [String] Unicode attack string
|
|
409
|
+
#
|
|
410
|
+
def unicode_attack
|
|
411
|
+
UNICODE_ATTACKS.sample(random: @rng.instance_variable_get(:@random))
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Factory method to create mode instance
|
|
417
|
+
#
|
|
418
|
+
# Creates an appropriate mode instance based on the mode symbol.
|
|
419
|
+
#
|
|
420
|
+
# @param mode [Symbol] mode name (:random, :edge, :invalid, :mixed)
|
|
421
|
+
# @param rng [Generator::RNG] random number generator
|
|
422
|
+
# @return [BaseMode] mode instance
|
|
423
|
+
# @raise [ArgumentError] if mode is unknown
|
|
424
|
+
#
|
|
425
|
+
# @example
|
|
426
|
+
# Modes.create(:edge, rng)
|
|
427
|
+
# # => #<EdgeMode>
|
|
428
|
+
#
|
|
429
|
+
|
|
430
|
+
def self.create(mode, rng)
|
|
431
|
+
case mode
|
|
432
|
+
when :random then RandomMode.new(rng)
|
|
433
|
+
when :edge then EdgeMode.new(rng)
|
|
434
|
+
when :invalid then InvalidMode.new(rng)
|
|
435
|
+
when :hostile then HostileMode.new(rng)
|
|
436
|
+
when :mixed then MixedMode.new(rng)
|
|
437
|
+
else
|
|
438
|
+
raise ArgumentError, "Unknown mode: #{mode}"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Generator
|
|
5
|
+
|
|
6
|
+
# Resolves cross-schema references
|
|
7
|
+
class Resolver
|
|
8
|
+
def initialize(registry)
|
|
9
|
+
@registry = registry
|
|
10
|
+
@cache = {}
|
|
11
|
+
@generating = Set.new
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Resolve a reference to another schema's field
|
|
17
|
+
#
|
|
18
|
+
# @param schema_name [String] the schema name
|
|
19
|
+
# @param field_path [String] dot-separated field path
|
|
20
|
+
# @param depth [Integer] current recursion depth (default: 0)
|
|
21
|
+
# @param seed [Integer, nil] deterministic seed for the referenced schema's generation
|
|
22
|
+
# (derived from the parent RNG by the Reference type) — makes ref() values reproducible
|
|
23
|
+
# @return [Object] the resolved value
|
|
24
|
+
# @raise [RecursionLimitError] if recursion depth exceeds configured limit
|
|
25
|
+
|
|
26
|
+
def resolve(schema_name, field_path, depth: 0, seed: nil)
|
|
27
|
+
|
|
28
|
+
# Validate recursion depth against configured limits (before mutex to avoid deadlock)
|
|
29
|
+
# Check depth + 1 because we're about to generate at that depth
|
|
30
|
+
Synthra.configuration.limits.validate_recursion!(depth + 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Check cache first (thread-safe read)
|
|
34
|
+
cache_key = "#{schema_name}.#{field_path}"
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
return @cache[cache_key] if @cache.key?(cache_key)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Check for cycles BEFORE acquiring mutex to avoid deadlock on recursive calls
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
if @generating.include?(schema_name)
|
|
43
|
+
cycle = @generating.to_a + [schema_name]
|
|
44
|
+
raise CyclicReferenceError, cycle
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Generate the referenced schema (outside mutex to allow recursive calls)
|
|
50
|
+
begin
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
@generating.add(schema_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
schema = @registry.schema(schema_name)
|
|
56
|
+
|
|
57
|
+
# Pass this resolver instance to ensure cycle detection works
|
|
58
|
+
# The Engine will reuse this resolver instead of creating a new one.
|
|
59
|
+
# `seed` (from the parent RNG) makes the referenced record reproducible by seed.
|
|
60
|
+
record = schema.generate(seed: seed, registry: @registry, depth: depth + 1, resolver: self)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Cache all fields from this schema
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
record.each do |key, val|
|
|
66
|
+
@cache["#{schema_name}.#{key}"] = val
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Extract the specific field
|
|
71
|
+
value = extract_path(record, field_path)
|
|
72
|
+
@cache[cache_key] = value
|
|
73
|
+
value
|
|
74
|
+
end
|
|
75
|
+
rescue KeyError
|
|
76
|
+
raise MissingReferenceError.new(schema_name: schema_name, field_path: field_path)
|
|
77
|
+
ensure
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
@generating.delete(schema_name)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Get schemas in dependency order
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<String>] schema names ordered by dependencies
|
|
88
|
+
|
|
89
|
+
def dependency_order
|
|
90
|
+
dependencies = build_dependency_graph
|
|
91
|
+
topological_sort(dependencies)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Detect cycles in schema references
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<Array<String>>] array of cycle paths
|
|
98
|
+
|
|
99
|
+
def detect_cycles
|
|
100
|
+
cycles = []
|
|
101
|
+
visited = Set.new
|
|
102
|
+
stack = []
|
|
103
|
+
|
|
104
|
+
@registry.names.each do |name|
|
|
105
|
+
detect_cycles_from(name, visited, stack, cycles)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
cycles
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Clear the resolution cache
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
|
|
116
|
+
def clear_cache
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
@cache.clear
|
|
119
|
+
@generating.clear
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Extract value from record using dot-separated path
|
|
127
|
+
#
|
|
128
|
+
# Traverses nested hashes using dot notation (e.g., "address.city").
|
|
129
|
+
#
|
|
130
|
+
# @param record [Hash] record to extract from
|
|
131
|
+
# @param path [String] dot-separated field path
|
|
132
|
+
# @return [Object, nil] extracted value or nil if not found
|
|
133
|
+
#
|
|
134
|
+
|
|
135
|
+
def extract_path(record, path)
|
|
136
|
+
parts = path.to_s.split(".")
|
|
137
|
+
value = record
|
|
138
|
+
|
|
139
|
+
parts.each do |part|
|
|
140
|
+
case value
|
|
141
|
+
when Hash
|
|
142
|
+
value = value[part] || value[part.to_sym]
|
|
143
|
+
else
|
|
144
|
+
return nil
|
|
145
|
+
end
|
|
146
|
+
return nil if value.nil?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
value
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Build dependency graph from schema references
|
|
154
|
+
#
|
|
155
|
+
# Analyzes all schemas to build a graph of cross-schema dependencies.
|
|
156
|
+
#
|
|
157
|
+
# @return [Hash] dependency graph { schema_name => [dependent_schemas] }
|
|
158
|
+
#
|
|
159
|
+
|
|
160
|
+
def build_dependency_graph
|
|
161
|
+
graph = Hash.new { |h, k| h[k] = [] }
|
|
162
|
+
|
|
163
|
+
@registry.each do |name, schema|
|
|
164
|
+
schema.fields.each do |field|
|
|
165
|
+
if field.type_name == "reference"
|
|
166
|
+
ref = field.type_args[:ref]
|
|
167
|
+
ref_schema = ref.respond_to?(:schema_name) ? ref.schema_name : ref&.dig(:schema_name)
|
|
168
|
+
graph[name] << ref_schema if ref_schema
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
graph
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Topologically sort schemas by dependencies
|
|
178
|
+
#
|
|
179
|
+
# Orders schemas so that dependencies come before dependents.
|
|
180
|
+
# Uses Kahn's algorithm for topological sorting.
|
|
181
|
+
#
|
|
182
|
+
# @param graph [Hash] dependency graph
|
|
183
|
+
# @return [Array<String>] sorted schema names
|
|
184
|
+
#
|
|
185
|
+
|
|
186
|
+
def topological_sort(graph)
|
|
187
|
+
in_degree = Hash.new(0)
|
|
188
|
+
all_nodes = Set.new(@registry.names)
|
|
189
|
+
|
|
190
|
+
graph.each do |node, deps|
|
|
191
|
+
deps.each { |dep| in_degree[dep] += 1 }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
queue = all_nodes.select { |n| in_degree[n].zero? }
|
|
195
|
+
result = []
|
|
196
|
+
|
|
197
|
+
until queue.empty?
|
|
198
|
+
node = queue.shift
|
|
199
|
+
result << node
|
|
200
|
+
|
|
201
|
+
graph[node].each do |dep|
|
|
202
|
+
in_degree[dep] -= 1
|
|
203
|
+
queue << dep if in_degree[dep].zero?
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# If we couldn't sort all, there's a cycle - just return all names
|
|
209
|
+
result.length == all_nodes.length ? result : @registry.names
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Detect cycles starting from a node
|
|
214
|
+
#
|
|
215
|
+
# Recursively traverses the dependency graph to detect cycles.
|
|
216
|
+
# Uses depth-first search with cycle detection.
|
|
217
|
+
#
|
|
218
|
+
# @param node [String] schema name to start from
|
|
219
|
+
# @param visited [Set] set of visited nodes
|
|
220
|
+
# @param stack [Array] current path stack
|
|
221
|
+
# @param cycles [Array] array to collect detected cycles
|
|
222
|
+
# @return [void]
|
|
223
|
+
#
|
|
224
|
+
|
|
225
|
+
def detect_cycles_from(node, visited, stack, cycles)
|
|
226
|
+
return if visited.include?(node)
|
|
227
|
+
|
|
228
|
+
if stack.include?(node)
|
|
229
|
+
cycle_start = stack.index(node)
|
|
230
|
+
cycles << stack[cycle_start..] + [node]
|
|
231
|
+
return
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
stack.push(node)
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
schema = @registry.schema(node)
|
|
238
|
+
schema.fields.each do |field|
|
|
239
|
+
if field.type_name == "reference"
|
|
240
|
+
ref = field.type_args[:ref]
|
|
241
|
+
ref_schema = ref.respond_to?(:schema_name) ? ref.schema_name : ref&.dig(:schema_name)
|
|
242
|
+
detect_cycles_from(ref_schema, visited, stack, cycles) if ref_schema
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
rescue KeyError
|
|
246
|
+
|
|
247
|
+
# Schema not found
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
stack.pop
|
|
251
|
+
visited.add(node)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|