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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# Synthra String Distance Utilities
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Provides string distance algorithms for "did you mean" suggestions in
|
|
9
|
+
# error messages. The primary algorithm is Levenshtein distance.
|
|
10
|
+
#
|
|
11
|
+
# @example Find similar strings
|
|
12
|
+
# StringDistance.levenshtein("uuid", "ulid") # => 2
|
|
13
|
+
# StringDistance.similar_strings("uui", ["uuid", "ulid", "name"], max_distance: 2)
|
|
14
|
+
# # => ["uuid", "ulid"]
|
|
15
|
+
#
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
module Synthra
|
|
20
|
+
module Utils
|
|
21
|
+
|
|
22
|
+
# String distance calculations for "did you mean" suggestions
|
|
23
|
+
#
|
|
24
|
+
# Provides methods to calculate edit distance between strings and
|
|
25
|
+
# find similar strings from a collection.
|
|
26
|
+
#
|
|
27
|
+
# @example Find similar type names
|
|
28
|
+
# StringDistance.similar_strings("strig", Synthra::Types::Registry.names)
|
|
29
|
+
# # => ["string", "text"]
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
module StringDistance
|
|
33
|
+
class << self
|
|
34
|
+
# Calculate Levenshtein distance between two strings
|
|
35
|
+
#
|
|
36
|
+
# The Levenshtein distance is the minimum number of single-character
|
|
37
|
+
# edits (insertions, deletions, or substitutions) required to
|
|
38
|
+
# transform one string into another.
|
|
39
|
+
#
|
|
40
|
+
# @param str1 [String] first string
|
|
41
|
+
# @param str2 [String] second string
|
|
42
|
+
# @return [Integer] the edit distance
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# StringDistance.levenshtein("uuid", "ulid") # => 2
|
|
46
|
+
# StringDistance.levenshtein("text", "text") # => 0
|
|
47
|
+
# StringDistance.levenshtein("", "abc") # => 3
|
|
48
|
+
#
|
|
49
|
+
def levenshtein(str1, str2)
|
|
50
|
+
str1 = str1.to_s.downcase
|
|
51
|
+
str2 = str2.to_s.downcase
|
|
52
|
+
|
|
53
|
+
return str2.length if str1.empty?
|
|
54
|
+
return str1.length if str2.empty?
|
|
55
|
+
return 0 if str1 == str2
|
|
56
|
+
|
|
57
|
+
# Create matrix
|
|
58
|
+
m = str1.length
|
|
59
|
+
n = str2.length
|
|
60
|
+
d = Array.new(m + 1) { Array.new(n + 1) }
|
|
61
|
+
|
|
62
|
+
# Initialize first column
|
|
63
|
+
(0..m).each { |i| d[i][0] = i }
|
|
64
|
+
|
|
65
|
+
# Initialize first row
|
|
66
|
+
(0..n).each { |j| d[0][j] = j }
|
|
67
|
+
|
|
68
|
+
# Fill in the rest of the matrix
|
|
69
|
+
(1..m).each do |i|
|
|
70
|
+
(1..n).each do |j|
|
|
71
|
+
cost = str1[i - 1] == str2[j - 1] ? 0 : 1
|
|
72
|
+
d[i][j] = [
|
|
73
|
+
d[i - 1][j] + 1, # deletion
|
|
74
|
+
d[i][j - 1] + 1, # insertion
|
|
75
|
+
d[i - 1][j - 1] + cost # substitution
|
|
76
|
+
].min
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
d[m][n]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Find strings similar to a target from a collection
|
|
84
|
+
#
|
|
85
|
+
# Returns strings that are within the specified edit distance of
|
|
86
|
+
# the target, sorted by distance (closest first).
|
|
87
|
+
#
|
|
88
|
+
# @param target [String] the string to match
|
|
89
|
+
# @param candidates [Array<String>] collection to search
|
|
90
|
+
# @param max_distance [Integer] maximum edit distance (default: 3)
|
|
91
|
+
# @param limit [Integer] maximum number of results (default: 3)
|
|
92
|
+
# @return [Array<String>] similar strings, sorted by distance
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# StringDistance.similar_strings("uui", ["uuid", "ulid", "name"])
|
|
96
|
+
# # => ["uuid"]
|
|
97
|
+
#
|
|
98
|
+
# StringDistance.similar_strings("tex", ["text", "next", "email"], max_distance: 2)
|
|
99
|
+
# # => ["text", "next"]
|
|
100
|
+
#
|
|
101
|
+
def similar_strings(target, candidates, max_distance: 3, limit: 3)
|
|
102
|
+
target = target.to_s
|
|
103
|
+
|
|
104
|
+
candidates
|
|
105
|
+
.map { |c| [c, levenshtein(target, c)] }
|
|
106
|
+
.select { |_, dist| dist <= max_distance }
|
|
107
|
+
.sort_by { |_, dist| dist }
|
|
108
|
+
.first(limit)
|
|
109
|
+
.map(&:first)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Format "did you mean" suggestion text
|
|
113
|
+
#
|
|
114
|
+
# @param suggestions [Array<String>] list of suggestions
|
|
115
|
+
# @return [String, nil] formatted suggestion text or nil if empty
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# StringDistance.format_suggestions(["uuid", "ulid"])
|
|
119
|
+
# # => "Did you mean: uuid, ulid?"
|
|
120
|
+
#
|
|
121
|
+
# StringDistance.format_suggestions([])
|
|
122
|
+
# # => nil
|
|
123
|
+
#
|
|
124
|
+
def format_suggestions(suggestions)
|
|
125
|
+
return nil if suggestions.nil? || suggestions.empty?
|
|
126
|
+
|
|
127
|
+
"Did you mean: #{suggestions.join(", ")}?"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
#
|
|
7
|
+
# Synthra Simulyra DSL Validator
|
|
8
|
+
# =============================================================================
|
|
9
|
+
#
|
|
10
|
+
# Validates the Simulyra DSL structure according to the strict separation
|
|
11
|
+
# rules between Schema, API, and Scenario layers.
|
|
12
|
+
#
|
|
13
|
+
# Validation Rules:
|
|
14
|
+
# - No duplicate schema names
|
|
15
|
+
# - All schema(:Name) references resolve to existing schemas
|
|
16
|
+
# - APIs have at least one scenario
|
|
17
|
+
# - Scenarios have @status directive
|
|
18
|
+
# - Probability sum does not exceed 100%
|
|
19
|
+
# - No HTTP directives in schema blocks
|
|
20
|
+
# - No schema field definitions in API blocks
|
|
21
|
+
#
|
|
22
|
+
# @example Validate a ProgramNode
|
|
23
|
+
# validator = DslValidator.new(program_node)
|
|
24
|
+
# validator.validate!
|
|
25
|
+
#
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
module Synthra
|
|
29
|
+
module Validator
|
|
30
|
+
# Validates Simulyra DSL AST structure
|
|
31
|
+
#
|
|
32
|
+
# The DslValidator checks that the DSL follows the strict separation
|
|
33
|
+
# between Schema, API, and Scenario layers.
|
|
34
|
+
#
|
|
35
|
+
# @example Validate all rules
|
|
36
|
+
# validator = DslValidator.new(program_node)
|
|
37
|
+
# errors = validator.validate # Returns array of ValidationError
|
|
38
|
+
#
|
|
39
|
+
# @example Raise on first error
|
|
40
|
+
# validator = DslValidator.new(program_node)
|
|
41
|
+
# validator.validate! # Raises ValidationError
|
|
42
|
+
#
|
|
43
|
+
class DslValidator
|
|
44
|
+
# HTTP-related directives that are only valid in scenarios
|
|
45
|
+
HTTP_DIRECTIVES = %w[status probability latency header method].freeze
|
|
46
|
+
|
|
47
|
+
# Valid HTTP status codes
|
|
48
|
+
VALID_STATUS_CODES = (100..599).freeze
|
|
49
|
+
|
|
50
|
+
# @param program [Parser::AST::ProgramNode] the AST to validate
|
|
51
|
+
def initialize(program)
|
|
52
|
+
@program = program
|
|
53
|
+
@errors = []
|
|
54
|
+
@schema_names = Set.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Validate the DSL and return all errors
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Errors::ValidationError>] list of validation errors
|
|
60
|
+
#
|
|
61
|
+
def validate
|
|
62
|
+
@errors = []
|
|
63
|
+
@schema_names = Set.new
|
|
64
|
+
|
|
65
|
+
# Collect all schema names first (for reference validation)
|
|
66
|
+
collect_schema_names
|
|
67
|
+
|
|
68
|
+
# Run all validation rules
|
|
69
|
+
validate_no_duplicate_schemas
|
|
70
|
+
validate_schema_references
|
|
71
|
+
validate_apis_have_scenarios
|
|
72
|
+
validate_scenarios_have_status
|
|
73
|
+
validate_probability_sums
|
|
74
|
+
validate_schema_context
|
|
75
|
+
validate_api_context
|
|
76
|
+
|
|
77
|
+
@errors
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Validate and raise on first error
|
|
81
|
+
#
|
|
82
|
+
# @raise [Errors::ValidationError] if any validation fails
|
|
83
|
+
# @return [true] if all validations pass
|
|
84
|
+
#
|
|
85
|
+
def validate!
|
|
86
|
+
errors = validate
|
|
87
|
+
raise errors.first if errors.any?
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Collect all schema names for reference validation
|
|
94
|
+
#
|
|
95
|
+
# @return [void]
|
|
96
|
+
#
|
|
97
|
+
def collect_schema_names
|
|
98
|
+
@program.schemas.each do |schema|
|
|
99
|
+
@schema_names.add(schema.name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Also collect legacy schema names
|
|
103
|
+
@program.legacy_schemas.each do |schema|
|
|
104
|
+
@schema_names.add(schema.name)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Validate no duplicate schema names
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
#
|
|
112
|
+
def validate_no_duplicate_schemas
|
|
113
|
+
seen = {}
|
|
114
|
+
|
|
115
|
+
all_schemas = @program.schemas + @program.legacy_schemas
|
|
116
|
+
all_schemas.each do |schema|
|
|
117
|
+
if seen.key?(schema.name)
|
|
118
|
+
@errors << Errors::SchemaError.new(
|
|
119
|
+
"Duplicate schema \"#{schema.name}\"",
|
|
120
|
+
line: schema.line,
|
|
121
|
+
suggestion: "Defined again at line #{schema.line}. Schema names must be unique."
|
|
122
|
+
)
|
|
123
|
+
else
|
|
124
|
+
seen[schema.name] = schema
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Validate that all schema(:Name) references resolve
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
#
|
|
133
|
+
def validate_schema_references
|
|
134
|
+
# Check schema references in schemas
|
|
135
|
+
all_schemas = @program.schemas + @program.legacy_schemas
|
|
136
|
+
all_schemas.each do |schema|
|
|
137
|
+
schema.fields.each do |field|
|
|
138
|
+
validate_type_references(field.type_node, schema.name)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Check schema references in API scenarios
|
|
143
|
+
@program.apis.each do |api|
|
|
144
|
+
api.scenarios.each do |scenario|
|
|
145
|
+
scenario.response_fields.each do |field|
|
|
146
|
+
validate_type_references(field.type_node, "API #{api.method.upcase} #{api.path}")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Validate type node for schema references
|
|
153
|
+
#
|
|
154
|
+
# @param type_node [Parser::AST::TypeNode] the type to validate
|
|
155
|
+
# @param context [String] context for error message
|
|
156
|
+
# @return [void]
|
|
157
|
+
#
|
|
158
|
+
def validate_type_references(type_node, context)
|
|
159
|
+
return unless type_node.is_a?(Parser::AST::SchemaReferenceNode)
|
|
160
|
+
|
|
161
|
+
unless @schema_names.include?(type_node.schema_name)
|
|
162
|
+
similar = find_similar_names(type_node.schema_name, @schema_names.to_a)
|
|
163
|
+
suggestion = similar.any? ? "Did you mean: #{similar.join(', ')}?" : nil
|
|
164
|
+
|
|
165
|
+
@errors << Errors::ReferenceError.new(
|
|
166
|
+
"Schema \"#{type_node.schema_name}\" is not defined",
|
|
167
|
+
line: type_node.line,
|
|
168
|
+
suggestion: "#{suggestion} Used in #{context}."
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Validate that APIs have at least one scenario
|
|
174
|
+
#
|
|
175
|
+
# @return [void]
|
|
176
|
+
#
|
|
177
|
+
def validate_apis_have_scenarios
|
|
178
|
+
@program.apis.each do |api|
|
|
179
|
+
if api.scenarios.empty?
|
|
180
|
+
@errors << Errors::ApiError.new(
|
|
181
|
+
"API #{api.method.upcase} #{api.path} must define at least one scenario",
|
|
182
|
+
line: api.line,
|
|
183
|
+
suggestion: "Add a scenario block with @status directive."
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Validate that scenarios have @status directive
|
|
190
|
+
#
|
|
191
|
+
# @return [void]
|
|
192
|
+
#
|
|
193
|
+
def validate_scenarios_have_status
|
|
194
|
+
@program.apis.each do |api|
|
|
195
|
+
api.scenarios.each do |scenario|
|
|
196
|
+
unless scenario.status_code
|
|
197
|
+
@errors << Errors::ScenarioError.new(
|
|
198
|
+
"Scenario \"#{scenario.name}\" must define @status",
|
|
199
|
+
line: scenario.line,
|
|
200
|
+
suggestion: "Required for HTTP simulation. Example: @status 200"
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Also validate status code is valid
|
|
205
|
+
if scenario.status_code && !VALID_STATUS_CODES.include?(scenario.status_code)
|
|
206
|
+
@errors << Errors::ScenarioError.new(
|
|
207
|
+
"Invalid status code #{scenario.status_code} in scenario \"#{scenario.name}\"",
|
|
208
|
+
line: scenario.line,
|
|
209
|
+
suggestion: "Status code must be between 100 and 599."
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Validate that probability sums don't exceed 100%
|
|
217
|
+
#
|
|
218
|
+
# @return [void]
|
|
219
|
+
#
|
|
220
|
+
def validate_probability_sums
|
|
221
|
+
@program.apis.each do |api|
|
|
222
|
+
total = 0
|
|
223
|
+
api.scenarios.each do |scenario|
|
|
224
|
+
total += scenario.probability if scenario.probability
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if total > 100
|
|
228
|
+
@errors << Errors::ScenarioError.new(
|
|
229
|
+
"Total scenario probability exceeds 100%",
|
|
230
|
+
line: api.line,
|
|
231
|
+
suggestion: "Current total: #{total}% in API #{api.method.upcase} #{api.path}."
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validate schema context (no HTTP directives in schemas)
|
|
238
|
+
#
|
|
239
|
+
# @return [void]
|
|
240
|
+
#
|
|
241
|
+
def validate_schema_context
|
|
242
|
+
all_schemas = @program.schemas + @program.legacy_schemas
|
|
243
|
+
all_schemas.each do |schema|
|
|
244
|
+
schema.behaviors.each do |behavior|
|
|
245
|
+
if HTTP_DIRECTIVES.include?(behavior.name.to_s)
|
|
246
|
+
@errors << Errors::ContextError.new(
|
|
247
|
+
"HTTP directives are not allowed inside schema definitions",
|
|
248
|
+
line: behavior.line,
|
|
249
|
+
suggestion: "@#{behavior.name} is an HTTP directive. Use it in scenario blocks only."
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Validate API context (scenarios should be properly nested)
|
|
257
|
+
#
|
|
258
|
+
# @return [void]
|
|
259
|
+
#
|
|
260
|
+
def validate_api_context
|
|
261
|
+
@program.apis.each do |api|
|
|
262
|
+
# Check for duplicate scenario names within an API
|
|
263
|
+
seen_scenarios = {}
|
|
264
|
+
api.scenarios.each do |scenario|
|
|
265
|
+
if seen_scenarios.key?(scenario.name)
|
|
266
|
+
@errors << Errors::ScenarioError.new(
|
|
267
|
+
"Duplicate scenario name \"#{scenario.name}\" in API #{api.method.upcase} #{api.path}",
|
|
268
|
+
line: scenario.line,
|
|
269
|
+
suggestion: "Scenario names must be unique within an API."
|
|
270
|
+
)
|
|
271
|
+
else
|
|
272
|
+
seen_scenarios[scenario.name] = scenario
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Check for multiple @status directives in a scenario
|
|
277
|
+
api.scenarios.each do |scenario|
|
|
278
|
+
status_count = scenario.directives.count { |d| d.is_a?(Parser::AST::StatusDirective) }
|
|
279
|
+
if status_count > 1
|
|
280
|
+
@errors << Errors::ScenarioError.new(
|
|
281
|
+
"Multiple @status directives are not allowed",
|
|
282
|
+
line: scenario.line,
|
|
283
|
+
suggestion: "First defined in scenario \"#{scenario.name}\". Use only one @status per scenario."
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Find similar names using basic string distance
|
|
291
|
+
#
|
|
292
|
+
# @param target [String] the name to match
|
|
293
|
+
# @param candidates [Array<String>] possible matches
|
|
294
|
+
# @return [Array<String>] similar names
|
|
295
|
+
#
|
|
296
|
+
def find_similar_names(target, candidates, max_results: 3)
|
|
297
|
+
scored = candidates.map do |name|
|
|
298
|
+
[name, levenshtein_distance(target.downcase, name.downcase)]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
scored.select { |_, dist| dist <= 3 }
|
|
302
|
+
.sort_by { |_, dist| dist }
|
|
303
|
+
.first(max_results)
|
|
304
|
+
.map(&:first)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Simple Levenshtein distance implementation
|
|
308
|
+
#
|
|
309
|
+
# @param s1 [String] first string
|
|
310
|
+
# @param s2 [String] second string
|
|
311
|
+
# @return [Integer] edit distance
|
|
312
|
+
#
|
|
313
|
+
def levenshtein_distance(s1, s2)
|
|
314
|
+
m = s1.length
|
|
315
|
+
n = s2.length
|
|
316
|
+
return n if m.zero?
|
|
317
|
+
return m if n.zero?
|
|
318
|
+
|
|
319
|
+
d = Array.new(m + 1) { Array.new(n + 1, 0) }
|
|
320
|
+
|
|
321
|
+
(0..m).each { |i| d[i][0] = i }
|
|
322
|
+
(0..n).each { |j| d[0][j] = j }
|
|
323
|
+
|
|
324
|
+
(1..m).each do |i|
|
|
325
|
+
(1..n).each do |j|
|
|
326
|
+
cost = s1[i - 1] == s2[j - 1] ? 0 : 1
|
|
327
|
+
d[i][j] = [
|
|
328
|
+
d[i - 1][j] + 1, # deletion
|
|
329
|
+
d[i][j - 1] + 1, # insertion
|
|
330
|
+
d[i - 1][j - 1] + cost # substitution
|
|
331
|
+
].min
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
d[m][n]
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|