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,533 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# Synthra Registry
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# The Registry class manages loading and storing multiple schema definitions.
|
|
9
|
+
# It's essential for handling cross-schema references (Ref(User.id)) and
|
|
10
|
+
# for organizing related schemas.
|
|
11
|
+
#
|
|
12
|
+
# @example Loading multiple schemas
|
|
13
|
+
# registry = Synthra::Registry.new
|
|
14
|
+
# registry.load_file("schemas/user.dsl")
|
|
15
|
+
# registry.load_file("schemas/order.dsl")
|
|
16
|
+
#
|
|
17
|
+
# user = registry.schema("User").generate
|
|
18
|
+
# order = registry.schema("Order").generate(registry: registry)
|
|
19
|
+
#
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
module Synthra
|
|
24
|
+
|
|
25
|
+
# Registry for storing and managing multiple schemas
|
|
26
|
+
#
|
|
27
|
+
# The Registry provides a central store for schema definitions, enabling:
|
|
28
|
+
# - Loading multiple DSL files
|
|
29
|
+
# - Cross-schema references (Ref(User.id))
|
|
30
|
+
# - Schema lookup by name
|
|
31
|
+
# - Enumeration of all schemas
|
|
32
|
+
#
|
|
33
|
+
# When generating data with cross-schema references, pass the registry
|
|
34
|
+
# to the generate method to enable reference resolution.
|
|
35
|
+
#
|
|
36
|
+
# @example Basic usage
|
|
37
|
+
# registry = Synthra::Registry.new
|
|
38
|
+
# registry.load_file("schemas/user.dsl")
|
|
39
|
+
#
|
|
40
|
+
# user_schema = registry.schema("User")
|
|
41
|
+
# user = user_schema.generate
|
|
42
|
+
#
|
|
43
|
+
# @example Multiple schemas with references
|
|
44
|
+
# registry = Synthra::Registry.new
|
|
45
|
+
# registry.load_file("schemas/user.dsl") # User: id, name, ...
|
|
46
|
+
# registry.load_file("schemas/order.dsl") # Order: user_id: Ref(User.id)
|
|
47
|
+
#
|
|
48
|
+
# order = registry.schema("Order").generate(registry: registry)
|
|
49
|
+
# # order["user_id"] will be a generated User.id
|
|
50
|
+
#
|
|
51
|
+
# @example Iterating over schemas
|
|
52
|
+
# registry.each do |name, schema|
|
|
53
|
+
# puts "Schema: #{name} with #{schema.fields.length} fields"
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
|
|
57
|
+
class Registry
|
|
58
|
+
|
|
59
|
+
# Create a new empty registry
|
|
60
|
+
#
|
|
61
|
+
# @return [Registry] a new registry instance
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# registry = Synthra::Registry.new
|
|
65
|
+
# registry.size # => 0
|
|
66
|
+
#
|
|
67
|
+
|
|
68
|
+
def initialize
|
|
69
|
+
@schemas = {}
|
|
70
|
+
@mutex = Mutex.new
|
|
71
|
+
@ast_cache = {}
|
|
72
|
+
@cache_mutex = Mutex.new
|
|
73
|
+
@max_cache_size = Synthra.configuration.registry_cache_size
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Load schemas from a DSL file
|
|
78
|
+
#
|
|
79
|
+
# Reads and parses the DSL file, adding all schemas defined in it
|
|
80
|
+
# to the registry. Existing schemas with the same name will be
|
|
81
|
+
# overwritten.
|
|
82
|
+
#
|
|
83
|
+
# @param path [String] path to the DSL file (relative or absolute)
|
|
84
|
+
# @param validate [Boolean] whether to validate copy() paths (default: config setting)
|
|
85
|
+
# @return [Array<Schema>] the schemas that were loaded
|
|
86
|
+
# @raise [Errors::ParseError] if the DSL syntax is invalid
|
|
87
|
+
# @raise [Errors::SyntaxError] if there are syntax errors
|
|
88
|
+
# @raise [Errors::PathError] if copy() path validation fails
|
|
89
|
+
# @raise [Errno::ENOENT] if the file doesn't exist
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# registry.load_file("schemas/user.dsl")
|
|
93
|
+
# # => [#<Schema User fields=5 behaviors=0>]
|
|
94
|
+
#
|
|
95
|
+
# @example Handle errors
|
|
96
|
+
# begin
|
|
97
|
+
# registry.load_file("invalid.dsl")
|
|
98
|
+
# rescue Synthra::ParseError => e
|
|
99
|
+
# puts "Error at line #{e.line}: #{e.message}"
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
# @example Skip validation
|
|
103
|
+
# registry.load_file("schema.dsl", validate: false)
|
|
104
|
+
#
|
|
105
|
+
|
|
106
|
+
def load_file(path, validate: Synthra.configuration.validate_paths_on_load)
|
|
107
|
+
schemas = @mutex.synchronize do
|
|
108
|
+
load_file_unsafe(path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Validate paths outside mutex to avoid deadlock
|
|
112
|
+
validate_paths! if validate
|
|
113
|
+
|
|
114
|
+
schemas
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def load_file_unsafe(path)
|
|
120
|
+
|
|
121
|
+
# Check cache first
|
|
122
|
+
cache_key = File.expand_path(path)
|
|
123
|
+
mtime = File.mtime(path) if File.exist?(path)
|
|
124
|
+
|
|
125
|
+
cached = @cache_mutex.synchronize do
|
|
126
|
+
cache_entry = @ast_cache[cache_key]
|
|
127
|
+
if cache_entry && cache_entry[:mtime] == mtime
|
|
128
|
+
|
|
129
|
+
# Update access time for LRU tracking
|
|
130
|
+
cache_entry[:accessed_at] = Time.now
|
|
131
|
+
cache_entry[:ast_nodes]
|
|
132
|
+
else
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if cached
|
|
138
|
+
|
|
139
|
+
# Use cached AST. Normalize through extract_schema_nodes so the new Simulyra
|
|
140
|
+
# (ProgramNode) syntax works here too — load_string already does this; load_file
|
|
141
|
+
# used to blindly #map the raw parse result and broke on every v2 .dsl file.
|
|
142
|
+
extract_schema_nodes(cached).map do |node|
|
|
143
|
+
schema = node.to_schema
|
|
144
|
+
@schemas[schema.name] = schema
|
|
145
|
+
schema
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
|
|
149
|
+
# Parse and cache
|
|
150
|
+
content = File.read(path)
|
|
151
|
+
parser = Parser::Parser.new
|
|
152
|
+
ast_nodes = parser.parse(content)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Cache the AST with LRU eviction
|
|
156
|
+
@cache_mutex.synchronize do
|
|
157
|
+
|
|
158
|
+
# Limit cache size using LRU (Least Recently Used) eviction
|
|
159
|
+
if @ast_cache.size >= @max_cache_size
|
|
160
|
+
|
|
161
|
+
# Remove least recently used entry
|
|
162
|
+
oldest_key = @ast_cache.min_by { |_, v| v[:accessed_at] || Time.at(0) }[0]
|
|
163
|
+
@ast_cache.delete(oldest_key)
|
|
164
|
+
end
|
|
165
|
+
@ast_cache[cache_key] = { ast_nodes: ast_nodes, mtime: mtime, accessed_at: Time.now }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
extract_schema_nodes(ast_nodes).map do |node|
|
|
169
|
+
schema = node.to_schema
|
|
170
|
+
@schemas[schema.name] = schema
|
|
171
|
+
schema
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
public
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Load schemas from all DSL files in a directory
|
|
180
|
+
#
|
|
181
|
+
# Loads and parses all .dsl files in the specified directory, adding
|
|
182
|
+
# all schemas defined in them to the registry. Existing schemas with
|
|
183
|
+
# the same name will be overwritten.
|
|
184
|
+
#
|
|
185
|
+
# @param dir [String] directory path containing .dsl files
|
|
186
|
+
# @param validate [Boolean] whether to validate copy() paths (default: config setting)
|
|
187
|
+
# @return [Array<Schema>] all schemas that were loaded
|
|
188
|
+
# @raise [Errno::ENOENT] if the directory doesn't exist
|
|
189
|
+
# @raise [Errors::ParseError] if any DSL file has syntax errors
|
|
190
|
+
# @raise [Errors::PathError] if copy() path validation fails
|
|
191
|
+
#
|
|
192
|
+
# @example
|
|
193
|
+
# registry.load_dir("schemas/")
|
|
194
|
+
# # => [#<Schema User>, #<Schema Order>, #<Schema Product>]
|
|
195
|
+
#
|
|
196
|
+
# @example Handle errors
|
|
197
|
+
# begin
|
|
198
|
+
# registry.load_dir("nonexistent/")
|
|
199
|
+
# rescue Errno::ENOENT => e
|
|
200
|
+
# puts "Directory not found: #{e.message}"
|
|
201
|
+
# end
|
|
202
|
+
#
|
|
203
|
+
# @example Skip validation
|
|
204
|
+
# registry.load_dir("schemas/", validate: false)
|
|
205
|
+
#
|
|
206
|
+
|
|
207
|
+
def load_dir(dir, validate: Synthra.configuration.validate_paths_on_load)
|
|
208
|
+
schemas = @mutex.synchronize do
|
|
209
|
+
|
|
210
|
+
# Validate and expand directory path
|
|
211
|
+
dir = File.expand_path(dir)
|
|
212
|
+
raise Errno::ENOENT, dir unless Dir.exist?(dir)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Find all .dsl files
|
|
216
|
+
pattern = File.join(dir, "*.dsl")
|
|
217
|
+
files = Dir.glob(pattern)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Load each file
|
|
221
|
+
all_schemas = []
|
|
222
|
+
files.each do |file|
|
|
223
|
+
# Call load_file_unsafe to avoid double synchronization (we're already in @mutex)
|
|
224
|
+
all_schemas.concat(load_file_unsafe(file))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
all_schemas
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Validate paths outside mutex to avoid deadlock
|
|
231
|
+
validate_paths! if validate
|
|
232
|
+
|
|
233
|
+
schemas
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Load schemas from a DSL string
|
|
238
|
+
#
|
|
239
|
+
# Parses the DSL source code and adds all schemas to the registry.
|
|
240
|
+
# Useful for testing or when DSL content is dynamically generated.
|
|
241
|
+
#
|
|
242
|
+
# @param source [String] DSL source code
|
|
243
|
+
# @param validate [Boolean] whether to validate copy() paths (default: config setting)
|
|
244
|
+
# @return [Array<Schema>] the schemas that were parsed
|
|
245
|
+
# @raise [Errors::ParseError] if the DSL syntax is invalid
|
|
246
|
+
# @raise [Errors::PathError] if copy() path validation fails
|
|
247
|
+
#
|
|
248
|
+
# @example
|
|
249
|
+
# dsl = <<~DSL
|
|
250
|
+
# User:
|
|
251
|
+
# id: uuid
|
|
252
|
+
# name: name
|
|
253
|
+
# DSL
|
|
254
|
+
#
|
|
255
|
+
# schemas = registry.load_string(dsl)
|
|
256
|
+
# schemas.first.name # => "User"
|
|
257
|
+
#
|
|
258
|
+
# @example Skip validation
|
|
259
|
+
# schemas = registry.load_string(dsl, validate: false)
|
|
260
|
+
#
|
|
261
|
+
|
|
262
|
+
def load_string(source, validate: Synthra.configuration.validate_paths_on_load)
|
|
263
|
+
schemas = @mutex.synchronize do
|
|
264
|
+
parser = Parser::Parser.new
|
|
265
|
+
parse_result = parser.parse(source)
|
|
266
|
+
|
|
267
|
+
# Handle both legacy (array of SchemaNode) and Simulyra (ProgramNode) syntax
|
|
268
|
+
ast_nodes = extract_schema_nodes(parse_result)
|
|
269
|
+
|
|
270
|
+
ast_nodes.map do |node|
|
|
271
|
+
schema = node.to_schema
|
|
272
|
+
@schemas[schema.name] = schema
|
|
273
|
+
schema
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Validate paths outside mutex to avoid deadlock
|
|
278
|
+
validate_paths! if validate
|
|
279
|
+
|
|
280
|
+
schemas
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
# Extract schema nodes from parse result
|
|
286
|
+
# Handles both legacy (Array) and Simulyra (ProgramNode) syntax
|
|
287
|
+
#
|
|
288
|
+
# @param parse_result [Array, ProgramNode] parsed result
|
|
289
|
+
# @return [Array<SchemaNode>] array of schema nodes
|
|
290
|
+
#
|
|
291
|
+
def extract_schema_nodes(parse_result)
|
|
292
|
+
if parse_result.is_a?(Parser::AST::ProgramNode)
|
|
293
|
+
# Simulyra DSL syntax - combine all schemas
|
|
294
|
+
all_schemas = []
|
|
295
|
+
all_schemas.concat(parse_result.schemas) if parse_result.schemas
|
|
296
|
+
all_schemas.concat(parse_result.legacy_schemas) if parse_result.legacy_schemas
|
|
297
|
+
all_schemas
|
|
298
|
+
elsif parse_result.is_a?(Array)
|
|
299
|
+
# Legacy syntax - already an array of schema nodes
|
|
300
|
+
parse_result
|
|
301
|
+
else
|
|
302
|
+
# Single node - wrap in array
|
|
303
|
+
[parse_result].compact
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
public
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Get a schema by name
|
|
311
|
+
#
|
|
312
|
+
# Retrieves a schema from the registry. Raises KeyError if not found.
|
|
313
|
+
#
|
|
314
|
+
# @param name [String, Symbol] schema name
|
|
315
|
+
# @return [Schema] the schema
|
|
316
|
+
# @raise [KeyError] if schema is not found
|
|
317
|
+
#
|
|
318
|
+
# @example
|
|
319
|
+
# user_schema = registry.schema("User")
|
|
320
|
+
# user_schema.fields.length # => 5
|
|
321
|
+
#
|
|
322
|
+
# @example Handle missing schema
|
|
323
|
+
# begin
|
|
324
|
+
# registry.schema("NonExistent")
|
|
325
|
+
# rescue KeyError => e
|
|
326
|
+
# puts e.message # => "Schema not found: NonExistent"
|
|
327
|
+
# end
|
|
328
|
+
#
|
|
329
|
+
|
|
330
|
+
def schema(name)
|
|
331
|
+
@mutex.synchronize do
|
|
332
|
+
@schemas.fetch(name.to_s) do
|
|
333
|
+
raise KeyError, "Schema not found: #{name}"
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Register an already-built Schema object under a name.
|
|
339
|
+
#
|
|
340
|
+
# Counterpart to load_string/load_file for callers that construct a Schema directly (OpenAPI
|
|
341
|
+
# import, ActiveRecord inference, the CLI/API server). Was referenced in those paths but never
|
|
342
|
+
# defined, so every one of them raised NoMethodError.
|
|
343
|
+
#
|
|
344
|
+
# @param name [String, Symbol] the schema name
|
|
345
|
+
# @param schema [Schema] the schema to register
|
|
346
|
+
# @return [Schema] the registered schema
|
|
347
|
+
#
|
|
348
|
+
def register_schema(name, schema)
|
|
349
|
+
@mutex.synchronize do
|
|
350
|
+
@schemas[name.to_s] = schema
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Get all registered schemas as a hash
|
|
356
|
+
#
|
|
357
|
+
# Returns a copy of the internal schemas hash to prevent modification.
|
|
358
|
+
#
|
|
359
|
+
# @return [Hash<String, Schema>] schema name => Schema mapping
|
|
360
|
+
#
|
|
361
|
+
# @example
|
|
362
|
+
# all_schemas = registry.schemas
|
|
363
|
+
# all_schemas.keys # => ["User", "Order"]
|
|
364
|
+
#
|
|
365
|
+
|
|
366
|
+
def schemas
|
|
367
|
+
@mutex.synchronize do
|
|
368
|
+
@schemas.dup
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# Check if a schema exists
|
|
374
|
+
#
|
|
375
|
+
# @param name [String, Symbol] schema name to check
|
|
376
|
+
# @return [Boolean] true if the schema exists
|
|
377
|
+
#
|
|
378
|
+
# @example
|
|
379
|
+
# registry.schema?("User") # => true
|
|
380
|
+
# registry.schema?("Unknown") # => false
|
|
381
|
+
#
|
|
382
|
+
|
|
383
|
+
def schema?(name)
|
|
384
|
+
@mutex.synchronize do
|
|
385
|
+
@schemas.key?(name.to_s)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# Unregister a schema by name
|
|
391
|
+
#
|
|
392
|
+
# Removes a single schema from the registry.
|
|
393
|
+
#
|
|
394
|
+
# @param name [String] schema name to remove
|
|
395
|
+
# @return [Schema, nil] the removed schema, or nil if not found
|
|
396
|
+
#
|
|
397
|
+
# @example
|
|
398
|
+
# registry.unregister("User")
|
|
399
|
+
#
|
|
400
|
+
def unregister(name)
|
|
401
|
+
@mutex.synchronize do
|
|
402
|
+
@schemas.delete(name.to_s)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Clear all registered schemas
|
|
408
|
+
#
|
|
409
|
+
# Removes all schemas from the registry.
|
|
410
|
+
#
|
|
411
|
+
# @return [void]
|
|
412
|
+
#
|
|
413
|
+
# @example
|
|
414
|
+
# registry.clear
|
|
415
|
+
# registry.size # => 0
|
|
416
|
+
#
|
|
417
|
+
|
|
418
|
+
def clear
|
|
419
|
+
@mutex.synchronize do
|
|
420
|
+
@schemas.clear
|
|
421
|
+
@cache_mutex.synchronize do
|
|
422
|
+
@ast_cache.clear
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# Get the number of registered schemas
|
|
429
|
+
#
|
|
430
|
+
# @return [Integer] number of schemas in the registry
|
|
431
|
+
#
|
|
432
|
+
# @example
|
|
433
|
+
# registry.load_file("user.dsl")
|
|
434
|
+
# registry.size # => 1
|
|
435
|
+
#
|
|
436
|
+
|
|
437
|
+
def size
|
|
438
|
+
@mutex.synchronize do
|
|
439
|
+
@schemas.size
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# Alias for size
|
|
445
|
+
# @return [Integer]
|
|
446
|
+
alias count size
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# Alias for size
|
|
450
|
+
# @return [Integer]
|
|
451
|
+
alias length size
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# Iterate over all schemas
|
|
455
|
+
#
|
|
456
|
+
# Yields each schema name and schema object to the block.
|
|
457
|
+
#
|
|
458
|
+
# @yield [name, schema] block to execute for each schema
|
|
459
|
+
# @yieldparam name [String] the schema name
|
|
460
|
+
# @yieldparam schema [Schema] the schema object
|
|
461
|
+
# @return [Enumerator, void] returns Enumerator if no block given
|
|
462
|
+
#
|
|
463
|
+
# @example
|
|
464
|
+
# registry.each do |name, schema|
|
|
465
|
+
# puts "#{name}: #{schema.fields.length} fields"
|
|
466
|
+
# end
|
|
467
|
+
#
|
|
468
|
+
# @example Get as array
|
|
469
|
+
# pairs = registry.each.to_a
|
|
470
|
+
# # => [["User", #<Schema>], ["Order", #<Schema>]]
|
|
471
|
+
#
|
|
472
|
+
|
|
473
|
+
def each(&block)
|
|
474
|
+
@mutex.synchronize do
|
|
475
|
+
@schemas.each(&block)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# Get all schema names
|
|
481
|
+
#
|
|
482
|
+
# @return [Array<String>] list of schema names
|
|
483
|
+
#
|
|
484
|
+
# @example
|
|
485
|
+
# registry.names # => ["User", "Order", "Product"]
|
|
486
|
+
#
|
|
487
|
+
|
|
488
|
+
def names
|
|
489
|
+
@mutex.synchronize do
|
|
490
|
+
@schemas.keys
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# Validate all copy() paths in the registry
|
|
496
|
+
#
|
|
497
|
+
# Checks that all copy() type references point to valid paths
|
|
498
|
+
# in the schema structure. This catches typos and invalid
|
|
499
|
+
# references at load time rather than at generation time.
|
|
500
|
+
#
|
|
501
|
+
# @return [Array<Errors::PathError>] list of validation errors (empty if valid)
|
|
502
|
+
#
|
|
503
|
+
# @example
|
|
504
|
+
# errors = registry.validate_paths
|
|
505
|
+
# errors.each { |e| puts e.message }
|
|
506
|
+
#
|
|
507
|
+
|
|
508
|
+
def validate_paths
|
|
509
|
+
# Don't hold mutex during validation - the validator caches schemas
|
|
510
|
+
# to avoid deadlock
|
|
511
|
+
validator = Validator::PathValidator.new(self)
|
|
512
|
+
validator.validate
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# Validate all copy() paths, raising on first error
|
|
517
|
+
#
|
|
518
|
+
# @raise [Errors::PathError] if any invalid path is found
|
|
519
|
+
# @return [true] if all paths are valid
|
|
520
|
+
#
|
|
521
|
+
# @example
|
|
522
|
+
# registry.load_file("schema.dsl")
|
|
523
|
+
# registry.validate_paths! # Raises if copy() paths are invalid
|
|
524
|
+
#
|
|
525
|
+
|
|
526
|
+
def validate_paths!
|
|
527
|
+
# Don't hold mutex during validation - the validator caches schemas
|
|
528
|
+
# to avoid deadlock
|
|
529
|
+
validator = Validator::PathValidator.new(self)
|
|
530
|
+
validator.validate!
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Data Relationships Support
|
|
5
|
+
#
|
|
6
|
+
# Provides expressive relationship types for schemas:
|
|
7
|
+
# - belongs_to(Schema) - Required reference
|
|
8
|
+
# - has_one(Schema) - Optional single reference
|
|
9
|
+
# - has_many(Schema, count) - Collection reference
|
|
10
|
+
#
|
|
11
|
+
# @example DSL syntax
|
|
12
|
+
# Order:
|
|
13
|
+
# id: uuid
|
|
14
|
+
# user: belongs_to(User)
|
|
15
|
+
# items: has_many(OrderItem, 1..10)
|
|
16
|
+
# shipping_address?: has_one(Address)
|
|
17
|
+
#
|
|
18
|
+
module Relationships
|
|
19
|
+
# Relationship type definitions
|
|
20
|
+
TYPES = {
|
|
21
|
+
belongs_to: { required: true, cardinality: :one },
|
|
22
|
+
has_one: { required: false, cardinality: :one },
|
|
23
|
+
has_many: { required: true, cardinality: :many }
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# Parse a relationship type from DSL
|
|
27
|
+
#
|
|
28
|
+
# @param type_str [String] e.g., "belongs_to(User)"
|
|
29
|
+
# @return [Hash, nil] parsed relationship or nil if not a relationship
|
|
30
|
+
#
|
|
31
|
+
def self.parse(type_str)
|
|
32
|
+
return nil unless type_str.is_a?(String)
|
|
33
|
+
|
|
34
|
+
TYPES.keys.each do |rel_type|
|
|
35
|
+
pattern = /^#{rel_type}\((\w+)(?:,\s*(\d+)\.\.(\d+))?\)$/
|
|
36
|
+
if (match = type_str.match(pattern))
|
|
37
|
+
return {
|
|
38
|
+
type: rel_type,
|
|
39
|
+
target_schema: match[1],
|
|
40
|
+
min_count: match[2]&.to_i || 1,
|
|
41
|
+
max_count: match[3]&.to_i || 1,
|
|
42
|
+
**TYPES[rel_type]
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if a type string is a relationship
|
|
51
|
+
#
|
|
52
|
+
# @param type_str [String]
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
#
|
|
55
|
+
def self.relationship?(type_str)
|
|
56
|
+
return false unless type_str.is_a?(String)
|
|
57
|
+
TYPES.keys.any? { |t| type_str.start_with?("#{t}(") }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert relationship to standard Synthra type
|
|
61
|
+
#
|
|
62
|
+
# @param relationship [Hash] parsed relationship
|
|
63
|
+
# @return [String] equivalent standard type
|
|
64
|
+
#
|
|
65
|
+
def self.to_standard_type(relationship)
|
|
66
|
+
case relationship[:cardinality]
|
|
67
|
+
when :one
|
|
68
|
+
relationship[:target_schema]
|
|
69
|
+
when :many
|
|
70
|
+
"array(#{relationship[:target_schema]}, #{relationship[:min_count]}..#{relationship[:max_count]})"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module Types
|
|
76
|
+
module Core
|
|
77
|
+
# BelongsTo type - required single reference
|
|
78
|
+
class BelongsTo < Base
|
|
79
|
+
def generate_random(rng, context, args)
|
|
80
|
+
target_schema = args[:schema] || args[:target]
|
|
81
|
+
return nil unless target_schema
|
|
82
|
+
|
|
83
|
+
if context.registry&.schema?(target_schema)
|
|
84
|
+
referenced_schema = context.registry.schema(target_schema)
|
|
85
|
+
id_field = referenced_schema.field("id")
|
|
86
|
+
if id_field
|
|
87
|
+
# Generate just the ID
|
|
88
|
+
Generator::Engine.new(referenced_schema, registry: context.registry)
|
|
89
|
+
.generate(mode: :random)["id"]
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
# Fallback: generate a UUID
|
|
93
|
+
SecureRandom.uuid
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def generate_edge(rng, context, args)
|
|
98
|
+
generate_random(rng, context, args)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def generate_invalid(rng, context, args)
|
|
102
|
+
rng.sample([nil, "", "invalid_id", 0, -1])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def generate_hostile(rng, context, args)
|
|
106
|
+
rng.sample([
|
|
107
|
+
"' OR '1'='1",
|
|
108
|
+
"1; DROP TABLE users;--",
|
|
109
|
+
"../../../etc/passwd",
|
|
110
|
+
"<script>alert(1)</script>"
|
|
111
|
+
])
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# HasOne type - optional single reference (generates full object)
|
|
116
|
+
class HasOne < Base
|
|
117
|
+
def generate_random(rng, context, args)
|
|
118
|
+
target_schema = args[:schema] || args[:target]
|
|
119
|
+
return nil unless target_schema
|
|
120
|
+
|
|
121
|
+
if context.registry&.schema?(target_schema)
|
|
122
|
+
referenced_schema = context.registry.schema(target_schema)
|
|
123
|
+
referenced_schema.generate(registry: context.registry, mode: :random)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def generate_edge(rng, context, args)
|
|
128
|
+
# Edge case: nil or empty
|
|
129
|
+
rng.boolean(0.5) ? nil : generate_random(rng, context, args)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def generate_invalid(rng, context, args)
|
|
133
|
+
rng.sample([nil, {}, [], "not_an_object"])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def generate_hostile(rng, context, args)
|
|
137
|
+
{ "injection" => "' OR '1'='1" }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# HasMany type - collection reference
|
|
142
|
+
class HasMany < Base
|
|
143
|
+
def generate_random(rng, context, args)
|
|
144
|
+
target_schema = args[:schema] || args[:target]
|
|
145
|
+
min_count = args[:min] || 1
|
|
146
|
+
max_count = args[:max] || 5
|
|
147
|
+
count = rng.int(min_count, max_count)
|
|
148
|
+
|
|
149
|
+
return [] unless target_schema
|
|
150
|
+
|
|
151
|
+
if context.registry&.schema?(target_schema)
|
|
152
|
+
referenced_schema = context.registry.schema(target_schema)
|
|
153
|
+
count.times.map do
|
|
154
|
+
referenced_schema.generate(registry: context.registry, mode: :random)
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
[]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_edge(rng, context, args)
|
|
162
|
+
# Edge cases: empty array, single item, max items
|
|
163
|
+
edge_counts = [0, 1, args[:max] || 10]
|
|
164
|
+
count = rng.sample(edge_counts)
|
|
165
|
+
|
|
166
|
+
target_schema = args[:schema] || args[:target]
|
|
167
|
+
return [] unless target_schema && count > 0
|
|
168
|
+
|
|
169
|
+
if context.registry&.schema?(target_schema)
|
|
170
|
+
referenced_schema = context.registry.schema(target_schema)
|
|
171
|
+
count.times.map do
|
|
172
|
+
referenced_schema.generate(registry: context.registry, mode: :edge)
|
|
173
|
+
end
|
|
174
|
+
else
|
|
175
|
+
[]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def generate_invalid(rng, context, args)
|
|
180
|
+
rng.sample([nil, "not_an_array", {}, [nil, nil]])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def generate_hostile(rng, context, args)
|
|
184
|
+
# Generate array with hostile payloads
|
|
185
|
+
[
|
|
186
|
+
{ "xss" => "<script>alert(1)</script>" },
|
|
187
|
+
{ "sqli" => "'; DROP TABLE users;--" }
|
|
188
|
+
]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|