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,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Types
|
|
5
|
+
# Regex type generator.
|
|
6
|
+
#
|
|
7
|
+
# Generates a string that MATCHES a supplied regular-expression pattern.
|
|
8
|
+
# The generator is small and dependency-free; it understands the common
|
|
9
|
+
# subset of regex syntax used in fake-data schemas:
|
|
10
|
+
#
|
|
11
|
+
# - character classes: \d \w \s \D \W \S and [a-z] [A-Z0-9] [^abc]
|
|
12
|
+
# - quantifiers: {n} {n,m} {n,} + * ?
|
|
13
|
+
# - literals: any other character is emitted verbatim
|
|
14
|
+
# - anchors: ^ and $ are stripped (treated as match boundaries)
|
|
15
|
+
# - groups: (...) are flattened (their contents are generated)
|
|
16
|
+
# - alternation: a|b picks one branch at random (seeded)
|
|
17
|
+
#
|
|
18
|
+
# Anything it cannot understand degrades gracefully to a reasonable string
|
|
19
|
+
# rather than crashing or returning "".
|
|
20
|
+
class Regex < Base
|
|
21
|
+
DEFAULT_REPEAT_MAX = 8 # cap for unbounded quantifiers (+ and *)
|
|
22
|
+
|
|
23
|
+
def generate_random(rng, context, args)
|
|
24
|
+
pattern = args[:pattern]
|
|
25
|
+
return "" unless pattern
|
|
26
|
+
|
|
27
|
+
source = pattern.is_a?(Regexp) ? pattern.source : pattern.to_s
|
|
28
|
+
return "" if source.empty?
|
|
29
|
+
|
|
30
|
+
build_from_pattern(source, rng)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# Pattern too complex / malformed — never crash, never return "".
|
|
33
|
+
"generated_string"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generate_edge(rng, context, args)
|
|
37
|
+
rng.sample(["", "0", "a"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_invalid(rng, context, args)
|
|
41
|
+
rng.sample([nil, 123, [], {}])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Generate a string matching +source+.
|
|
47
|
+
#
|
|
48
|
+
# @param source [String] regex source (no surrounding slashes)
|
|
49
|
+
# @param rng [Generator::RNG] seeded RNG
|
|
50
|
+
# @return [String]
|
|
51
|
+
def build_from_pattern(source, rng)
|
|
52
|
+
source = strip_anchors(source)
|
|
53
|
+
source = pick_alternation(source, rng)
|
|
54
|
+
tokens = tokenize(source)
|
|
55
|
+
tokens.map { |tok| render_token(tok, rng) }.join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Strip leading ^ and trailing $ anchors (they constrain position, not content).
|
|
59
|
+
def strip_anchors(source)
|
|
60
|
+
source.sub(/\A\^/, "").sub(/\$\z/, "")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# If the (top-level) pattern uses alternation, pick one branch deterministically.
|
|
64
|
+
# Only splits on bars that are not inside a group or char class.
|
|
65
|
+
def pick_alternation(source, rng)
|
|
66
|
+
branches = split_top_level_alternation(source)
|
|
67
|
+
return source if branches.length <= 1
|
|
68
|
+
|
|
69
|
+
rng.sample(branches)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def split_top_level_alternation(source)
|
|
73
|
+
branches = []
|
|
74
|
+
current = +""
|
|
75
|
+
depth = 0
|
|
76
|
+
in_class = false
|
|
77
|
+
i = 0
|
|
78
|
+
while i < source.length
|
|
79
|
+
ch = source[i]
|
|
80
|
+
case ch
|
|
81
|
+
when "\\"
|
|
82
|
+
current << ch << (source[i + 1] || "")
|
|
83
|
+
i += 2
|
|
84
|
+
next
|
|
85
|
+
when "["
|
|
86
|
+
in_class = true
|
|
87
|
+
current << ch
|
|
88
|
+
when "]"
|
|
89
|
+
in_class = false
|
|
90
|
+
current << ch
|
|
91
|
+
when "("
|
|
92
|
+
depth += 1 unless in_class
|
|
93
|
+
current << ch
|
|
94
|
+
when ")"
|
|
95
|
+
depth -= 1 unless in_class
|
|
96
|
+
current << ch
|
|
97
|
+
when "|"
|
|
98
|
+
if depth.zero? && !in_class
|
|
99
|
+
branches << current
|
|
100
|
+
current = +""
|
|
101
|
+
else
|
|
102
|
+
current << ch
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
current << ch
|
|
106
|
+
end
|
|
107
|
+
i += 1
|
|
108
|
+
end
|
|
109
|
+
branches << current
|
|
110
|
+
branches
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Break the pattern into (atom, quantifier) tokens.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<Hash>] each token: { atom:, min:, max: }
|
|
116
|
+
def tokenize(source)
|
|
117
|
+
tokens = []
|
|
118
|
+
i = 0
|
|
119
|
+
while i < source.length
|
|
120
|
+
atom, i = read_atom(source, i)
|
|
121
|
+
min, max, i = read_quantifier(source, i)
|
|
122
|
+
tokens << { atom: atom, min: min, max: max }
|
|
123
|
+
end
|
|
124
|
+
tokens
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Read a single atom starting at +i+. Returns [atom_descriptor, next_index].
|
|
128
|
+
# atom_descriptor is one of:
|
|
129
|
+
# { kind: :class, chars: [...] } - a character set to pick from
|
|
130
|
+
# { kind: :literal, char: "x" } - a literal character
|
|
131
|
+
def read_atom(source, i)
|
|
132
|
+
ch = source[i]
|
|
133
|
+
case ch
|
|
134
|
+
when "\\"
|
|
135
|
+
esc = source[i + 1]
|
|
136
|
+
[{ kind: :class, chars: escape_class(esc) }, i + 2]
|
|
137
|
+
when "["
|
|
138
|
+
chars, next_i = read_char_class(source, i + 1)
|
|
139
|
+
[{ kind: :class, chars: chars }, next_i]
|
|
140
|
+
when "."
|
|
141
|
+
[{ kind: :class, chars: printable_chars }, i + 1]
|
|
142
|
+
when "(", ")"
|
|
143
|
+
# Flatten groups: skip the paren, the contents are read as normal atoms.
|
|
144
|
+
[{ kind: :empty }, i + 1]
|
|
145
|
+
else
|
|
146
|
+
[{ kind: :literal, char: ch }, i + 1]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Expand an escaped class character (\d \w \s and negations) to a char list.
|
|
151
|
+
def escape_class(esc)
|
|
152
|
+
case esc
|
|
153
|
+
when "d" then ("0".."9").to_a
|
|
154
|
+
when "w" then ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + ["_"]
|
|
155
|
+
when "s" then [" ", "\t"]
|
|
156
|
+
when "D" then ("a".."z").to_a + ("A".."Z").to_a
|
|
157
|
+
when "W" then [" ", "-", ".", "!"]
|
|
158
|
+
when "S" then ("a".."z").to_a + ("0".."9").to_a
|
|
159
|
+
else [esc] # escaped literal, e.g. \. \- \/
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Read a [...] character class starting after the opening bracket.
|
|
164
|
+
# Returns [chars, next_index].
|
|
165
|
+
def read_char_class(source, i)
|
|
166
|
+
negate = false
|
|
167
|
+
if source[i] == "^"
|
|
168
|
+
negate = true
|
|
169
|
+
i += 1
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
chars = []
|
|
173
|
+
while i < source.length && source[i] != "]"
|
|
174
|
+
if source[i] == "\\"
|
|
175
|
+
chars.concat(escape_class(source[i + 1]))
|
|
176
|
+
i += 2
|
|
177
|
+
next
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if source[i + 1] == "-" && source[i + 2] && source[i + 2] != "]"
|
|
181
|
+
chars.concat((source[i]..source[i + 2]).to_a)
|
|
182
|
+
i += 3
|
|
183
|
+
else
|
|
184
|
+
chars << source[i]
|
|
185
|
+
i += 1
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
i += 1 # consume closing ]
|
|
189
|
+
|
|
190
|
+
chars = printable_chars - chars if negate
|
|
191
|
+
[chars, i]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Read a quantifier at +i+. Returns [min, max, next_index].
|
|
195
|
+
# No quantifier => [1, 1, i].
|
|
196
|
+
def read_quantifier(source, i)
|
|
197
|
+
case source[i]
|
|
198
|
+
when "+" then [1, DEFAULT_REPEAT_MAX, i + 1]
|
|
199
|
+
when "*" then [0, DEFAULT_REPEAT_MAX, i + 1]
|
|
200
|
+
when "?" then [0, 1, i + 1]
|
|
201
|
+
when "{" then read_brace_quantifier(source, i)
|
|
202
|
+
else [1, 1, i]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def read_brace_quantifier(source, i)
|
|
207
|
+
close = source.index("}", i)
|
|
208
|
+
return [1, 1, i] unless close
|
|
209
|
+
|
|
210
|
+
body = source[(i + 1)...close]
|
|
211
|
+
if body.include?(",")
|
|
212
|
+
lo, hi = body.split(",", 2)
|
|
213
|
+
min = lo.to_i
|
|
214
|
+
max = hi.empty? ? min + DEFAULT_REPEAT_MAX : hi.to_i
|
|
215
|
+
else
|
|
216
|
+
min = max = body.to_i
|
|
217
|
+
end
|
|
218
|
+
[min, max, close + 1]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def render_token(token, rng)
|
|
222
|
+
atom = token[:atom]
|
|
223
|
+
return "" if atom[:kind] == :empty
|
|
224
|
+
|
|
225
|
+
count = token[:min] == token[:max] ? token[:min] : rng.int(token[:min], token[:max])
|
|
226
|
+
Array.new(count) { render_atom(atom, rng) }.join
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def render_atom(atom, rng)
|
|
230
|
+
case atom[:kind]
|
|
231
|
+
when :class
|
|
232
|
+
chars = atom[:chars]
|
|
233
|
+
chars.empty? ? "" : rng.sample(chars)
|
|
234
|
+
when :literal
|
|
235
|
+
atom[:char]
|
|
236
|
+
else
|
|
237
|
+
# :nocov: render_token short-circuits :empty atoms, so this arm is a defensive fallback only
|
|
238
|
+
""
|
|
239
|
+
# :nocov:
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def printable_chars
|
|
244
|
+
@printable_chars ||= ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Synthra Types Registry
|
|
5
|
+
# =============================================================================
|
|
6
|
+
#
|
|
7
|
+
# The Types::Registry manages registration and lookup of type generator classes.
|
|
8
|
+
# Built-in types are registered automatically at gem load time.
|
|
9
|
+
#
|
|
10
|
+
# @example Register a custom type
|
|
11
|
+
# Synthra::Types::Registry.register("my_type", MyTypeClass)
|
|
12
|
+
#
|
|
13
|
+
# @example Register with a block
|
|
14
|
+
# Synthra::Types::Registry.register("my_type") do |rng, context, args, mode|
|
|
15
|
+
# "custom_value"
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
module Synthra
|
|
21
|
+
module Types
|
|
22
|
+
# Registry for type generator classes
|
|
23
|
+
#
|
|
24
|
+
# Thread-safe registry for storing and retrieving type implementations.
|
|
25
|
+
# Supports both class-based and block-based type registration.
|
|
26
|
+
#
|
|
27
|
+
# @example Look up a type
|
|
28
|
+
# type = Registry.lookup("uuid")
|
|
29
|
+
# value = type.generate(rng, context, {}, :random)
|
|
30
|
+
#
|
|
31
|
+
class Registry
|
|
32
|
+
# Internal storage for registered types
|
|
33
|
+
# @api private
|
|
34
|
+
@types = {}
|
|
35
|
+
|
|
36
|
+
# Mutex for thread-safe access
|
|
37
|
+
# @api private
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# Register a type generator
|
|
42
|
+
#
|
|
43
|
+
# @param name [String] type name (used in DSL)
|
|
44
|
+
# @param klass [Class, nil] type class (must inherit from Base)
|
|
45
|
+
# @param force [Boolean] if true, overwrites existing type
|
|
46
|
+
# @yield [rng, context, args, mode] block for generating values
|
|
47
|
+
# @yieldparam rng [Generator::RNG] random number generator
|
|
48
|
+
# @yieldparam context [Generator::Context] generation context
|
|
49
|
+
# @yieldparam args [Hash] type arguments
|
|
50
|
+
# @yieldparam mode [Symbol] generation mode (:random, :edge, :invalid)
|
|
51
|
+
# @yieldreturn [Object] generated value
|
|
52
|
+
# @return [void]
|
|
53
|
+
# @raise [ArgumentError] if type exists and force is false
|
|
54
|
+
#
|
|
55
|
+
# @example Register with a class
|
|
56
|
+
# Registry.register("uuid", Types::Uuid)
|
|
57
|
+
#
|
|
58
|
+
# @example Register with a block
|
|
59
|
+
# Registry.register("ssn") do |rng, context, args, mode|
|
|
60
|
+
# "#{rng.int(100, 999)}-#{rng.int(10, 99)}-#{rng.int(1000, 9999)}"
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
def register(name, klass = nil, force: false, &block)
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
key = name.to_s
|
|
66
|
+
if @types.key?(key) && !force
|
|
67
|
+
raise ArgumentError, "Type '#{name}' already registered. Use force: true to override."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if block
|
|
71
|
+
# Create a wrapper class for block-based types
|
|
72
|
+
wrapper = Class.new(Base) do
|
|
73
|
+
define_method(:generate_random) do |rng, context, args|
|
|
74
|
+
block.call(rng, context, args, :random)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
define_method(:generate_edge) do |rng, context, args|
|
|
78
|
+
block.call(rng, context, args, :edge)
|
|
79
|
+
|
|
80
|
+
rescue StandardError
|
|
81
|
+
block.call(rng, context, args, :random)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
define_method(:generate_invalid) do |rng, context, args|
|
|
86
|
+
block.call(rng, context, args, :invalid)
|
|
87
|
+
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
@types[key] = wrapper.new
|
|
94
|
+
elsif klass
|
|
95
|
+
@types[key] = klass.new
|
|
96
|
+
else
|
|
97
|
+
raise ArgumentError, "Must provide either a class or a block"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Look up a registered type
|
|
103
|
+
#
|
|
104
|
+
# @param name [String] type name (may include ? for nullable)
|
|
105
|
+
# @return [Base] type generator instance
|
|
106
|
+
# @raise [UnknownTypeError] if type not found (includes "did you mean" suggestions)
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# type = Registry.lookup("uuid")
|
|
110
|
+
# value = type.generate(rng, context, {}, :random)
|
|
111
|
+
#
|
|
112
|
+
def lookup(name)
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
# Strip nullable suffix if present
|
|
115
|
+
key = name.to_s.sub(/\?$/, "")
|
|
116
|
+
@types.fetch(key) do
|
|
117
|
+
# Find similar type names for "did you mean" suggestions
|
|
118
|
+
suggestions = find_similar_types(key)
|
|
119
|
+
raise Errors::UnknownTypeError.new(name, suggestions: suggestions)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Find type names similar to the given name
|
|
127
|
+
#
|
|
128
|
+
# @param name [String] the type name to match
|
|
129
|
+
# @return [Array<String>] similar type names
|
|
130
|
+
#
|
|
131
|
+
def find_similar_types(name)
|
|
132
|
+
Utils::StringDistance.similar_strings(name, @types.keys, max_distance: 3, limit: 3)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
public
|
|
136
|
+
|
|
137
|
+
# Check if a type is registered
|
|
138
|
+
#
|
|
139
|
+
# @param name [String] type name
|
|
140
|
+
# @return [Boolean] true if registered
|
|
141
|
+
#
|
|
142
|
+
def registered?(name)
|
|
143
|
+
@mutex.synchronize do
|
|
144
|
+
key = name.to_s.sub(/\?$/, "")
|
|
145
|
+
@types.key?(key)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get all registered type names
|
|
150
|
+
#
|
|
151
|
+
# @return [Array<String>] list of type names
|
|
152
|
+
#
|
|
153
|
+
def names
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
@types.keys.sort
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Clear all registered types
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
#
|
|
163
|
+
def clear
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@types.clear
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get the number of registered types
|
|
170
|
+
#
|
|
171
|
+
# @return [Integer] number of types
|
|
172
|
+
#
|
|
173
|
+
def size
|
|
174
|
+
@mutex.synchronize do
|
|
175
|
+
@types.size
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Types
|
|
5
|
+
# Repeating element type generator (stub implementation)
|
|
6
|
+
class RepeatingElement < Base
|
|
7
|
+
def generate_random(rng, context, args)
|
|
8
|
+
value = args[:value]
|
|
9
|
+
count = clamp_count(args[:count] || 1)
|
|
10
|
+
type_name = args[:type]
|
|
11
|
+
|
|
12
|
+
if type_name
|
|
13
|
+
generate_from_type(rng, context, type_name, count)
|
|
14
|
+
elsif value
|
|
15
|
+
value.to_s * count
|
|
16
|
+
else
|
|
17
|
+
""
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_edge(rng, context, args)
|
|
22
|
+
rng.sample(["", "-", "---"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate_invalid(rng, context, args)
|
|
26
|
+
rng.sample([nil, 123, [], {}])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Bound the repeat count to the same ceiling as the other array-producing
|
|
32
|
+
# types (schema_ref, array). Without this, a tenant-supplied `count:` allocates
|
|
33
|
+
# an unbounded Array/String — a denial-of-service vector. Floors at 0.
|
|
34
|
+
def clamp_count(count)
|
|
35
|
+
max = Synthra.configuration&.limits&.max_array_size || 1000
|
|
36
|
+
[[count.to_i, 0].max, max].min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_from_type(rng, context, type_name, count)
|
|
40
|
+
type = Types::Registry.lookup(type_name)
|
|
41
|
+
Array.new(count) { type.generate_random(rng, context, {}) }
|
|
42
|
+
|
|
43
|
+
rescue StandardError
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Types
|
|
5
|
+
# Scenario type generator (stub implementation)
|
|
6
|
+
class Scenario < Base
|
|
7
|
+
SCENARIOS = {
|
|
8
|
+
user_roles: ["admin", "user", "guest"],
|
|
9
|
+
status: ["active", "inactive", "pending"],
|
|
10
|
+
default: ["scenario1", "scenario2", "scenario3"]
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def generate_random(rng, context, args)
|
|
14
|
+
name = (args[:name] || :default).to_sym
|
|
15
|
+
scenarios = SCENARIOS[name] || SCENARIOS[:default]
|
|
16
|
+
rng.sample(scenarios)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_edge(rng, context, args)
|
|
20
|
+
rng.sample(["", "edge", "scenario"])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def generate_invalid(rng, context, args)
|
|
24
|
+
rng.sample([nil, 123, [], {}])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Types
|
|
5
|
+
module TechnologyInternet
|
|
6
|
+
# AppBundleId type
|
|
7
|
+
class AppBundleId < Base
|
|
8
|
+
def generate_random(rng, context, args)
|
|
9
|
+
# Format: com.company.appname
|
|
10
|
+
companies = ["company", "corp", "inc", "llc", "tech", "app", "mobile", "digital"]
|
|
11
|
+
apps = ["app", "application", "mobile", "ios", "android", "game", "tool", "utility"]
|
|
12
|
+
company = rng.sample(companies)
|
|
13
|
+
app = rng.sample(apps)
|
|
14
|
+
number = rng.int(1, 999)
|
|
15
|
+
"com.#{company}.#{app}#{number}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_edge(rng, context, args)
|
|
19
|
+
["", "com.a.b", "com.verylongcompanyname.verylongappname"].sample(random: rng.instance_variable_get(:@random))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_invalid(rng, context, args)
|
|
23
|
+
[nil, 123, [], {}].sample(random: rng.instance_variable_get(:@random))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# AppName type
|
|
28
|
+
class AppName < Base
|
|
29
|
+
APP_NAMES = [
|
|
30
|
+
"MyApp", "SuperApp", "AwesomeApp", "BestApp", "CoolApp",
|
|
31
|
+
"QuickApp", "SmartApp", "FastApp", "EasyApp", "ProApp"
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def generate_random(rng, context, args)
|
|
35
|
+
rng.sample(APP_NAMES)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_edge(rng, context, args)
|
|
39
|
+
["", "MyApp", "A"].sample(random: rng.instance_variable_get(:@random))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def generate_invalid(rng, context, args)
|
|
43
|
+
[nil, 123, [], {}].sample(random: rng.instance_variable_get(:@random))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# AppVersion type
|
|
48
|
+
class AppVersion < Base
|
|
49
|
+
def generate_random(rng, context, args)
|
|
50
|
+
# Semantic versioning: major.minor.patch
|
|
51
|
+
major = rng.int(0, 10)
|
|
52
|
+
minor = rng.int(0, 99)
|
|
53
|
+
patch = rng.int(0, 999)
|
|
54
|
+
"#{major}.#{minor}.#{patch}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generate_edge(rng, context, args)
|
|
58
|
+
["0.0.0", "10.99.999", "1.0.0"].sample(random: rng.instance_variable_get(:@random))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generate_invalid(rng, context, args)
|
|
62
|
+
[nil, "not a version", "invalid", [], {}].sample(random: rng.instance_variable_get(:@random))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|