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,615 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
# Synthra Path Validator
|
|
8
|
+
# =============================================================================
|
|
9
|
+
#
|
|
10
|
+
# Validates copy() path references against schema structure at parse/load time.
|
|
11
|
+
# This catches path typos and invalid references before data generation.
|
|
12
|
+
#
|
|
13
|
+
# @example Validate paths in a registry
|
|
14
|
+
# validator = PathValidator.new(registry)
|
|
15
|
+
# validator.validate!
|
|
16
|
+
#
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
module Synthra
|
|
21
|
+
module Validator
|
|
22
|
+
|
|
23
|
+
# Validates copy() path references against schema structure
|
|
24
|
+
#
|
|
25
|
+
# The PathValidator scans all schemas in a registry for copy() type
|
|
26
|
+
# fields and validates that the referenced paths actually exist.
|
|
27
|
+
# It handles:
|
|
28
|
+
# - Direct field references (e.g., "user_id")
|
|
29
|
+
# - Nested field references (e.g., "address.city")
|
|
30
|
+
# - map_by_field alias references (e.g., "users.sender.id")
|
|
31
|
+
#
|
|
32
|
+
# @example Validate all paths in a registry
|
|
33
|
+
# validator = PathValidator.new(registry)
|
|
34
|
+
# validator.validate! # Raises PathError if invalid paths found
|
|
35
|
+
#
|
|
36
|
+
# @example Get validation results without raising
|
|
37
|
+
# errors = validator.validate
|
|
38
|
+
# errors.each { |e| puts e.message }
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
class PathValidator
|
|
42
|
+
# @param registry [Registry] the registry containing schemas to validate
|
|
43
|
+
def initialize(registry)
|
|
44
|
+
@registry = registry
|
|
45
|
+
@path_index = {}
|
|
46
|
+
@copy_paths = []
|
|
47
|
+
# Cache schemas to avoid mutex deadlock when registry calls back
|
|
48
|
+
@schemas_cache = registry.schemas
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Validate all copy() paths in the registry
|
|
52
|
+
#
|
|
53
|
+
# @return [Array<PathError, CycleError>] list of validation errors (empty if valid)
|
|
54
|
+
#
|
|
55
|
+
def validate
|
|
56
|
+
build_path_index
|
|
57
|
+
collect_copy_paths
|
|
58
|
+
|
|
59
|
+
errors = []
|
|
60
|
+
errors.concat(validate_paths)
|
|
61
|
+
errors.concat(validate_field_order)
|
|
62
|
+
errors.concat(validate_ref_targets)
|
|
63
|
+
errors.concat(detect_cycles)
|
|
64
|
+
errors
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate all copy() paths, raising on first error
|
|
68
|
+
#
|
|
69
|
+
# @raise [Errors::PathError] if any invalid path is found
|
|
70
|
+
# @return [true] if all paths are valid
|
|
71
|
+
#
|
|
72
|
+
def validate!
|
|
73
|
+
errors = validate
|
|
74
|
+
raise errors.first if errors.any?
|
|
75
|
+
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Build an index of all available field paths in all schemas
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
#
|
|
85
|
+
def build_path_index
|
|
86
|
+
@path_index = {}
|
|
87
|
+
|
|
88
|
+
@schemas_cache.each do |name, schema|
|
|
89
|
+
# Track visited schemas per top-level schema to detect cycles
|
|
90
|
+
visited = Set.new
|
|
91
|
+
build_schema_paths(name, schema, "", visited)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build paths for a single schema
|
|
96
|
+
#
|
|
97
|
+
# @param name [String] schema name
|
|
98
|
+
# @param schema [Schema] the schema
|
|
99
|
+
# @param prefix [String] path prefix for nested fields
|
|
100
|
+
# @param visited [Set] schemas already visited in this path
|
|
101
|
+
# @return [void]
|
|
102
|
+
#
|
|
103
|
+
def build_schema_paths(name, schema, prefix, visited)
|
|
104
|
+
# Avoid infinite recursion on circular references
|
|
105
|
+
return if visited.include?(name)
|
|
106
|
+
visited.add(name)
|
|
107
|
+
|
|
108
|
+
schema.fields.each do |field|
|
|
109
|
+
field_path = prefix.empty? ? field.name : "#{prefix}.#{field.name}"
|
|
110
|
+
@path_index[field_path] = { schema: name, field: field }
|
|
111
|
+
|
|
112
|
+
# Handle nested schema types
|
|
113
|
+
if nested_schema?(field)
|
|
114
|
+
nested_name = field.type_name
|
|
115
|
+
if schema_exists?(nested_name) && !visited.include?(nested_name)
|
|
116
|
+
nested_schema = get_schema(nested_name)
|
|
117
|
+
build_schema_paths(nested_name, nested_schema, field_path, visited.dup)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Handle map_by_field entries
|
|
122
|
+
if map_by_field?(field)
|
|
123
|
+
build_map_by_field_paths(field, field_path, visited)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Handle array types with nested schemas
|
|
127
|
+
if array_with_nested_schema?(field)
|
|
128
|
+
element_type = field.type_args[:element]
|
|
129
|
+
if schema_exists?(element_type.to_s) && !visited.include?(element_type.to_s)
|
|
130
|
+
nested_schema = get_schema(element_type.to_s)
|
|
131
|
+
# Arrays can be accessed by index like "items.0.name"
|
|
132
|
+
build_schema_paths(element_type.to_s, nested_schema, "#{field_path}.0", visited.dup)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Build paths for map_by_field entries
|
|
139
|
+
#
|
|
140
|
+
# @param field [Field] the map_by_field field
|
|
141
|
+
# @param prefix [String] path prefix
|
|
142
|
+
# @param visited [Set] schemas already visited in this path
|
|
143
|
+
# @return [void]
|
|
144
|
+
#
|
|
145
|
+
def build_map_by_field_paths(field, prefix, visited)
|
|
146
|
+
entries = field.type_args[:entries] || {}
|
|
147
|
+
|
|
148
|
+
entries.each do |entry_name, entry_info|
|
|
149
|
+
entry_path = "#{prefix}.#{entry_name}"
|
|
150
|
+
@path_index[entry_path] = { schema: nil, field: field, entry: entry_name }
|
|
151
|
+
|
|
152
|
+
# Get the schema name (handle both string and hash formats)
|
|
153
|
+
schema_name = if entry_info.is_a?(Hash)
|
|
154
|
+
entry_info[:schema] || entry_info["schema"]
|
|
155
|
+
else
|
|
156
|
+
entry_info.to_s
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if schema_exists?(schema_name) && !visited.include?(schema_name)
|
|
160
|
+
nested_schema = get_schema(schema_name)
|
|
161
|
+
build_schema_paths(schema_name, nested_schema, entry_path, visited.dup)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Collect all copy() paths from all schemas
|
|
167
|
+
#
|
|
168
|
+
# @return [void]
|
|
169
|
+
#
|
|
170
|
+
def collect_copy_paths
|
|
171
|
+
@copy_paths = []
|
|
172
|
+
@collected_schemas = Set.new
|
|
173
|
+
|
|
174
|
+
@schemas_cache.each do |name, schema|
|
|
175
|
+
collect_schema_copy_paths(name, schema)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Collect copy() paths from a single schema
|
|
180
|
+
#
|
|
181
|
+
# @param name [String] schema name
|
|
182
|
+
# @param schema [Schema] the schema
|
|
183
|
+
# @return [void]
|
|
184
|
+
#
|
|
185
|
+
def collect_schema_copy_paths(name, schema)
|
|
186
|
+
# Avoid infinite recursion on circular references
|
|
187
|
+
return if @collected_schemas.include?(name)
|
|
188
|
+
@collected_schemas.add(name)
|
|
189
|
+
|
|
190
|
+
schema.fields.each do |field|
|
|
191
|
+
if copy_type?(field)
|
|
192
|
+
path = extract_copy_path(field)
|
|
193
|
+
@copy_paths << {
|
|
194
|
+
path: path,
|
|
195
|
+
schema: name,
|
|
196
|
+
field: field.name,
|
|
197
|
+
line: nil # Line info not available on Field
|
|
198
|
+
} if path
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Also check nested schemas recursively
|
|
202
|
+
if nested_schema?(field)
|
|
203
|
+
nested_name = field.type_name
|
|
204
|
+
if schema_exists?(nested_name) && !@collected_schemas.include?(nested_name)
|
|
205
|
+
nested_schema = get_schema(nested_name)
|
|
206
|
+
collect_schema_copy_paths(nested_name, nested_schema)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Validate all collected copy() paths
|
|
213
|
+
#
|
|
214
|
+
# @return [Array<PathError>] list of validation errors
|
|
215
|
+
#
|
|
216
|
+
def validate_paths
|
|
217
|
+
errors = []
|
|
218
|
+
|
|
219
|
+
@copy_paths.each do |path_info|
|
|
220
|
+
path = path_info[:path]
|
|
221
|
+
|
|
222
|
+
# Skip validation if path contains parent context indicators
|
|
223
|
+
# (these need runtime resolution)
|
|
224
|
+
next if path_needs_runtime_resolution?(path)
|
|
225
|
+
|
|
226
|
+
unless path_exists?(path)
|
|
227
|
+
suggestions = find_similar_paths(path)
|
|
228
|
+
errors << Errors::PathError.new(
|
|
229
|
+
path,
|
|
230
|
+
available_paths: @path_index.keys.first(20),
|
|
231
|
+
suggestions: suggestions,
|
|
232
|
+
line: path_info[:line]
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Also validate map_by_field key fields
|
|
238
|
+
errors.concat(validate_map_by_field_keys)
|
|
239
|
+
|
|
240
|
+
errors
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Validate that copy() fields don't reference later-defined fields
|
|
244
|
+
#
|
|
245
|
+
# copy() can only reference fields that are defined BEFORE it in the schema.
|
|
246
|
+
# This is because fields are generated in definition order, so referencing
|
|
247
|
+
# a later field would result in nil.
|
|
248
|
+
#
|
|
249
|
+
# @return [Array<Errors::FieldOrderError>] list of field order errors
|
|
250
|
+
#
|
|
251
|
+
def validate_field_order
|
|
252
|
+
errors = []
|
|
253
|
+
|
|
254
|
+
@schemas_cache.each do |schema_name, schema|
|
|
255
|
+
field_names = schema.fields.map(&:name)
|
|
256
|
+
|
|
257
|
+
schema.fields.each_with_index do |field, field_index|
|
|
258
|
+
next unless copy_type?(field)
|
|
259
|
+
|
|
260
|
+
copy_path = extract_copy_path(field)
|
|
261
|
+
next unless copy_path
|
|
262
|
+
|
|
263
|
+
# Get the root field name from the path (e.g., "users.sender.id" -> "users")
|
|
264
|
+
root_field = copy_path.split(".").first
|
|
265
|
+
next unless root_field
|
|
266
|
+
|
|
267
|
+
# Check if the root field is defined in this schema
|
|
268
|
+
root_index = field_names.index(root_field)
|
|
269
|
+
next unless root_index # Field is from parent context or nested schema
|
|
270
|
+
|
|
271
|
+
# If the copy references a field defined AFTER it, that's a problem
|
|
272
|
+
if root_index >= field_index
|
|
273
|
+
errors << Errors::FieldOrderError.new(
|
|
274
|
+
field_name: field.name,
|
|
275
|
+
copy_path: copy_path,
|
|
276
|
+
schema_name: schema_name,
|
|
277
|
+
suggestion: "Move '#{root_field}' field before '#{field.name}' in schema '#{schema_name}'"
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
errors
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Validate that Ref() targets exist and have the referenced field
|
|
287
|
+
#
|
|
288
|
+
# @return [Array<PathError>] list of validation errors
|
|
289
|
+
#
|
|
290
|
+
def validate_ref_targets
|
|
291
|
+
errors = []
|
|
292
|
+
|
|
293
|
+
@schemas_cache.each do |schema_name, schema|
|
|
294
|
+
schema.fields.each do |field|
|
|
295
|
+
next unless ref_type?(field)
|
|
296
|
+
|
|
297
|
+
ref_schema = field.type_args[:schema] || field.type_args["schema"]
|
|
298
|
+
ref_field = field.type_args[:field] || field.type_args["field"]
|
|
299
|
+
|
|
300
|
+
next unless ref_schema
|
|
301
|
+
|
|
302
|
+
# Check if referenced schema exists
|
|
303
|
+
unless schema_exists?(ref_schema)
|
|
304
|
+
similar = Utils::StringDistance.similar_strings(ref_schema, @schemas_cache.keys, max_distance: 2, limit: 3)
|
|
305
|
+
errors << Errors::PathError.new(
|
|
306
|
+
"Ref(#{ref_schema}.#{ref_field})",
|
|
307
|
+
suggestions: similar.map { |s| "Ref(#{s}.#{ref_field || 'id'})" },
|
|
308
|
+
line: nil
|
|
309
|
+
)
|
|
310
|
+
next
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Check if referenced field exists in target schema
|
|
314
|
+
if ref_field
|
|
315
|
+
target_schema = get_schema(ref_schema)
|
|
316
|
+
field_names = target_schema.fields.map(&:name)
|
|
317
|
+
|
|
318
|
+
unless field_names.include?(ref_field)
|
|
319
|
+
similar = Utils::StringDistance.similar_strings(ref_field, field_names, max_distance: 2, limit: 3)
|
|
320
|
+
errors << Errors::PathError.new(
|
|
321
|
+
"#{ref_schema}.#{ref_field}",
|
|
322
|
+
suggestions: similar.map { |f| "Ref(#{ref_schema}.#{f})" },
|
|
323
|
+
line: nil
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
errors
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Validate that map_by_field key fields exist in referenced schemas
|
|
334
|
+
#
|
|
335
|
+
# @return [Array<PathError>] list of validation errors
|
|
336
|
+
#
|
|
337
|
+
def validate_map_by_field_keys
|
|
338
|
+
errors = []
|
|
339
|
+
|
|
340
|
+
@schemas_cache.each do |schema_name, schema|
|
|
341
|
+
schema.fields.each do |field|
|
|
342
|
+
next unless map_by_field?(field)
|
|
343
|
+
|
|
344
|
+
key_field = field.type_args[:key_field] || field.type_args["key_field"] || "id"
|
|
345
|
+
entries = field.type_args[:entries] || field.type_args["entries"] || {}
|
|
346
|
+
|
|
347
|
+
entries.each do |entry_name, entry_info|
|
|
348
|
+
# Get the schema name (handle both string and hash formats)
|
|
349
|
+
entry_schema_name = if entry_info.is_a?(Hash)
|
|
350
|
+
entry_info[:schema] || entry_info["schema"]
|
|
351
|
+
else
|
|
352
|
+
entry_info.to_s
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
next unless entry_schema_name && schema_exists?(entry_schema_name)
|
|
356
|
+
|
|
357
|
+
entry_schema = get_schema(entry_schema_name)
|
|
358
|
+
field_names = entry_schema.fields.map(&:name)
|
|
359
|
+
|
|
360
|
+
unless field_names.include?(key_field)
|
|
361
|
+
similar = Utils::StringDistance.similar_strings(key_field, field_names, max_distance: 2, limit: 3)
|
|
362
|
+
errors << Errors::PathError.new(
|
|
363
|
+
"#{key_field}' in schema '#{entry_schema_name}",
|
|
364
|
+
suggestions: similar.map { |f| "#{entry_schema_name}.#{f}" },
|
|
365
|
+
line: nil
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
errors
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Check if a path exists in the index
|
|
376
|
+
#
|
|
377
|
+
# @param path [String] the path to check
|
|
378
|
+
# @return [Boolean] true if path exists
|
|
379
|
+
#
|
|
380
|
+
def path_exists?(path)
|
|
381
|
+
return true if @path_index.key?(path)
|
|
382
|
+
|
|
383
|
+
# Try partial path matching (for nested paths that may be dynamically generated)
|
|
384
|
+
parts = path.split(".")
|
|
385
|
+
current_path = ""
|
|
386
|
+
|
|
387
|
+
parts.each_with_index do |part, idx|
|
|
388
|
+
current_path = idx.zero? ? part : "#{current_path}.#{part}"
|
|
389
|
+
|
|
390
|
+
# Check if this is the last part
|
|
391
|
+
if idx == parts.length - 1
|
|
392
|
+
return @path_index.key?(current_path)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# If path doesn't exist and we're not at the end, check if we have a dynamic path
|
|
396
|
+
next if @path_index.key?(current_path)
|
|
397
|
+
|
|
398
|
+
# Check for array index patterns (e.g., "0" for first element)
|
|
399
|
+
return true if part =~ /^\d+$/
|
|
400
|
+
|
|
401
|
+
return false
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
false
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Check if a path needs runtime resolution
|
|
408
|
+
#
|
|
409
|
+
# @param path [String] the path to check
|
|
410
|
+
# @return [Boolean] true if path needs runtime resolution
|
|
411
|
+
#
|
|
412
|
+
def path_needs_runtime_resolution?(path)
|
|
413
|
+
# Paths with array indices may need runtime resolution
|
|
414
|
+
path =~ /\.\d+\./ || path.start_with?(".")
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Find paths similar to the given path
|
|
418
|
+
#
|
|
419
|
+
# @param path [String] the path to match
|
|
420
|
+
# @return [Array<String>] similar paths
|
|
421
|
+
#
|
|
422
|
+
def find_similar_paths(path)
|
|
423
|
+
Utils::StringDistance.similar_strings(path, @path_index.keys, max_distance: 3, limit: 3)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Check if field is a copy() type
|
|
427
|
+
#
|
|
428
|
+
# @param field [Field] the field to check
|
|
429
|
+
# @return [Boolean] true if copy type
|
|
430
|
+
#
|
|
431
|
+
def copy_type?(field)
|
|
432
|
+
field.type_name == "copy"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Extract the path argument from a copy() type
|
|
436
|
+
#
|
|
437
|
+
# @param field [Field] the copy field
|
|
438
|
+
# @return [String, nil] the path or nil
|
|
439
|
+
#
|
|
440
|
+
def extract_copy_path(field)
|
|
441
|
+
args = field.type_args
|
|
442
|
+
args[:path] || args["path"] || args.values.first
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Check if field is a nested schema type
|
|
446
|
+
#
|
|
447
|
+
# @param field [Field] the field to check
|
|
448
|
+
# @return [Boolean] true if nested schema
|
|
449
|
+
#
|
|
450
|
+
def nested_schema?(field)
|
|
451
|
+
type_name = field.type_name
|
|
452
|
+
return false unless type_name
|
|
453
|
+
|
|
454
|
+
# Not a builtin type and exists in registry
|
|
455
|
+
!Types::Registry.registered?(type_name) && schema_exists?(type_name)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Check if field is a map_by_field type
|
|
459
|
+
#
|
|
460
|
+
# @param field [Field] the field to check
|
|
461
|
+
# @return [Boolean] true if map_by_field
|
|
462
|
+
#
|
|
463
|
+
def map_by_field?(field)
|
|
464
|
+
field.type_name == "map_by_field"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Check if field is a Ref() type
|
|
468
|
+
#
|
|
469
|
+
# @param field [Field] the field to check
|
|
470
|
+
# @return [Boolean] true if Ref type
|
|
471
|
+
#
|
|
472
|
+
def ref_type?(field)
|
|
473
|
+
field.type_name == "reference" || field.type_name == "Ref"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Check if field is an array with nested schema
|
|
477
|
+
#
|
|
478
|
+
# @param field [Field] the field to check
|
|
479
|
+
# @return [Boolean] true if array with schema element
|
|
480
|
+
#
|
|
481
|
+
def array_with_nested_schema?(field)
|
|
482
|
+
return false unless field.type_name == "array"
|
|
483
|
+
|
|
484
|
+
element_type = field.type_args[:element]
|
|
485
|
+
return false unless element_type
|
|
486
|
+
|
|
487
|
+
schema_exists?(element_type.to_s)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Check if a schema exists (without mutex deadlock)
|
|
491
|
+
#
|
|
492
|
+
# @param name [String] schema name
|
|
493
|
+
# @return [Boolean] true if schema exists
|
|
494
|
+
#
|
|
495
|
+
def schema_exists?(name)
|
|
496
|
+
@schemas_cache.key?(name.to_s)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Get a schema (without mutex deadlock)
|
|
500
|
+
#
|
|
501
|
+
# @param name [String] schema name
|
|
502
|
+
# @return [Schema] the schema
|
|
503
|
+
#
|
|
504
|
+
def get_schema(name)
|
|
505
|
+
@schemas_cache[name.to_s]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Detect circular references in schema definitions
|
|
509
|
+
#
|
|
510
|
+
# @return [Array<CycleError>] list of cycle errors
|
|
511
|
+
#
|
|
512
|
+
def detect_cycles
|
|
513
|
+
errors = []
|
|
514
|
+
visited = {}
|
|
515
|
+
rec_stack = {}
|
|
516
|
+
|
|
517
|
+
@schemas_cache.each_key do |schema_name|
|
|
518
|
+
cycle = find_cycle(schema_name, visited, rec_stack, [])
|
|
519
|
+
errors << Errors::CycleError.new(cycle) if cycle
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
errors
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Find a cycle starting from the given schema
|
|
526
|
+
#
|
|
527
|
+
# @param name [String] schema name to start from
|
|
528
|
+
# @param visited [Hash] globally visited schemas
|
|
529
|
+
# @param rec_stack [Hash] current recursion stack
|
|
530
|
+
# @param path [Array<String>] current path through schemas
|
|
531
|
+
# @return [Array<String>, nil] cycle path if found, nil otherwise
|
|
532
|
+
#
|
|
533
|
+
def find_cycle(name, visited, rec_stack, path)
|
|
534
|
+
# Skip if already fully processed
|
|
535
|
+
return nil if visited[name]
|
|
536
|
+
|
|
537
|
+
# If we're back in the recursion stack, we found a cycle
|
|
538
|
+
if rec_stack[name]
|
|
539
|
+
cycle_start = path.index(name)
|
|
540
|
+
return path[cycle_start..] + [name]
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Skip if schema doesn't exist
|
|
544
|
+
return nil unless schema_exists?(name)
|
|
545
|
+
|
|
546
|
+
# Mark as being processed
|
|
547
|
+
rec_stack[name] = true
|
|
548
|
+
current_path = path + [name]
|
|
549
|
+
|
|
550
|
+
schema = get_schema(name)
|
|
551
|
+
return nil unless schema
|
|
552
|
+
|
|
553
|
+
# Check all referenced schemas
|
|
554
|
+
schema.fields.each do |field|
|
|
555
|
+
referenced_schemas = get_referenced_schemas(field)
|
|
556
|
+
|
|
557
|
+
referenced_schemas.each do |ref_name|
|
|
558
|
+
cycle = find_cycle(ref_name, visited, rec_stack, current_path)
|
|
559
|
+
return cycle if cycle
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Mark as fully processed
|
|
564
|
+
rec_stack[name] = false
|
|
565
|
+
visited[name] = true
|
|
566
|
+
nil
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# Get all schemas referenced by a field
|
|
570
|
+
#
|
|
571
|
+
# @param field [Field] the field to check
|
|
572
|
+
# @return [Array<String>] list of referenced schema names
|
|
573
|
+
#
|
|
574
|
+
def get_referenced_schemas(field)
|
|
575
|
+
refs = []
|
|
576
|
+
|
|
577
|
+
# Direct schema reference
|
|
578
|
+
if nested_schema?(field)
|
|
579
|
+
refs << field.type_name
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Array element type
|
|
583
|
+
if array_with_nested_schema?(field)
|
|
584
|
+
element_type = field.type_args[:element]
|
|
585
|
+
refs << element_type.to_s if element_type
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# map_by_field entries
|
|
589
|
+
if map_by_field?(field)
|
|
590
|
+
entries = field.type_args[:entries] || {}
|
|
591
|
+
entries.each do |_, entry_info|
|
|
592
|
+
schema_name = if entry_info.is_a?(Hash)
|
|
593
|
+
entry_info[:schema] || entry_info["schema"]
|
|
594
|
+
else
|
|
595
|
+
entry_info.to_s
|
|
596
|
+
end
|
|
597
|
+
refs << schema_name if schema_name
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# one_of schemas
|
|
602
|
+
if field.type_name == "one_of"
|
|
603
|
+
schemas = field.type_args[:schemas] || []
|
|
604
|
+
schemas.each do |s|
|
|
605
|
+
schema_name = s.is_a?(Hash) ? (s[:schema] || s["schema"]) : s.to_s
|
|
606
|
+
refs << schema_name if schema_name
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
refs.select { |r| schema_exists?(r) }
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# Synthra Version
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# This file defines the version constant for the Synthra gem.
|
|
9
|
+
# The version follows Semantic Versioning (https://semver.org/):
|
|
10
|
+
#
|
|
11
|
+
# MAJOR.MINOR.PATCH
|
|
12
|
+
# - MAJOR: Incremented for incompatible API changes
|
|
13
|
+
# - MINOR: Incremented for backward-compatible functionality additions
|
|
14
|
+
# - PATCH: Incremented for backward-compatible bug fixes
|
|
15
|
+
#
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# :nocov:
|
|
20
|
+
module Synthra
|
|
21
|
+
|
|
22
|
+
# Current version of the Synthra gem
|
|
23
|
+
#
|
|
24
|
+
# @return [String] the semantic version string
|
|
25
|
+
#
|
|
26
|
+
# @example Check version
|
|
27
|
+
# puts Synthra::VERSION # => "0.1.0"
|
|
28
|
+
#
|
|
29
|
+
# @example Compare versions
|
|
30
|
+
# Gem::Version.new(Synthra::VERSION) >= Gem::Version.new("0.1.0")
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
VERSION = "0.1.0"
|
|
34
|
+
end
|
|
35
|
+
# :nocov:
|