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,397 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Personas: Named Data Profiles
|
|
5
|
+
#
|
|
6
|
+
# Define reusable data profiles that represent different user types,
|
|
7
|
+
# test cases, or data states. More semantic than "modes" - think
|
|
8
|
+
# in terms of WHO you're generating data for.
|
|
9
|
+
#
|
|
10
|
+
# @example Define personas
|
|
11
|
+
# Synthra::Personas.define(:happy_path) do
|
|
12
|
+
# mode :random
|
|
13
|
+
# override User, email_verified: true, subscription: "premium"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Synthra::Personas.define(:edge_case_user) do
|
|
17
|
+
# mode :edge
|
|
18
|
+
# override User, name: "X", email: "a@b.co"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Use personas
|
|
22
|
+
# Synthra.as(:happy_path) { User.generate }
|
|
23
|
+
# Synthra.as(:edge_case_user) { User.generate }
|
|
24
|
+
#
|
|
25
|
+
module Personas
|
|
26
|
+
class << self
|
|
27
|
+
# Registry of defined personas
|
|
28
|
+
def registry
|
|
29
|
+
@registry ||= {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Define a new persona
|
|
33
|
+
#
|
|
34
|
+
# @param name [Symbol] persona name
|
|
35
|
+
# @yield persona configuration block
|
|
36
|
+
# @return [Persona] the defined persona
|
|
37
|
+
#
|
|
38
|
+
def define(name, &block)
|
|
39
|
+
persona = Persona.new(name)
|
|
40
|
+
persona.instance_eval(&block) if block
|
|
41
|
+
registry[name] = persona
|
|
42
|
+
persona
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get a persona by name
|
|
46
|
+
#
|
|
47
|
+
# @param name [Symbol] persona name
|
|
48
|
+
# @return [Persona, nil]
|
|
49
|
+
#
|
|
50
|
+
def [](name)
|
|
51
|
+
registry[name]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# List all personas
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<Symbol>]
|
|
57
|
+
#
|
|
58
|
+
def list
|
|
59
|
+
registry.keys
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Generate data using a persona
|
|
63
|
+
#
|
|
64
|
+
# @param persona_name [Symbol] persona to use
|
|
65
|
+
# @yield block for generation
|
|
66
|
+
# @return [Object] result of the block
|
|
67
|
+
#
|
|
68
|
+
def as(persona_name, &block)
|
|
69
|
+
persona = registry[persona_name]
|
|
70
|
+
raise ArgumentError, "Unknown persona: #{persona_name}" unless persona
|
|
71
|
+
|
|
72
|
+
persona.apply(&block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Load personas from configuration
|
|
76
|
+
#
|
|
77
|
+
# @param config [Hash] configuration hash
|
|
78
|
+
#
|
|
79
|
+
def load_config(config)
|
|
80
|
+
config.each do |name, definition|
|
|
81
|
+
define(name.to_sym) do
|
|
82
|
+
# :nocov: the `&.` nil-arm is unreachable — the trailing `if definition["mode"]` guard
|
|
83
|
+
# already guarantees definition["mode"] is truthy whenever this line runs.
|
|
84
|
+
mode definition["mode"]&.to_sym if definition["mode"]
|
|
85
|
+
# :nocov:
|
|
86
|
+
seed definition["seed"] if definition["seed"]
|
|
87
|
+
|
|
88
|
+
definition["overrides"]&.each do |schema, overrides|
|
|
89
|
+
override schema, **Personas.send(:symbolize_keys, overrides)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
definition["traits"]&.each do |trait|
|
|
93
|
+
include_trait trait.to_sym
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Load personas from YAML file
|
|
100
|
+
#
|
|
101
|
+
# @param path [String] path to YAML file
|
|
102
|
+
#
|
|
103
|
+
def load_file(path)
|
|
104
|
+
require "yaml"
|
|
105
|
+
config = YAML.safe_load(File.read(path), permitted_classes: [Symbol])
|
|
106
|
+
load_config(config["personas"] || config)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Reset all personas (clears custom personas, preserves built-ins)
|
|
110
|
+
def reset!
|
|
111
|
+
@registry = {}
|
|
112
|
+
define_built_in_personas!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Define built-in personas
|
|
116
|
+
def define_built_in_personas!
|
|
117
|
+
define(:default) do
|
|
118
|
+
mode :random
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
define(:happy_path) do
|
|
122
|
+
mode :random
|
|
123
|
+
# Most common, valid data
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
define(:edge_cases) do
|
|
127
|
+
mode :edge
|
|
128
|
+
# Boundary values, empty strings, max lengths
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
define(:invalid) do
|
|
132
|
+
mode :invalid
|
|
133
|
+
# Invalid data for validation testing
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
define(:security_test) do
|
|
137
|
+
mode :hostile
|
|
138
|
+
# SQL injection, XSS, malicious payloads
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
define(:stress_test) do
|
|
142
|
+
mode :edge
|
|
143
|
+
transform do |data, _context|
|
|
144
|
+
# Make strings very long
|
|
145
|
+
data.transform_values do |v|
|
|
146
|
+
v.is_a?(String) ? v * 100 : v
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def symbolize_keys(hash)
|
|
155
|
+
hash.transform_keys(&:to_sym)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# A persona definition
|
|
160
|
+
class Persona
|
|
161
|
+
attr_reader :name, :generation_mode, :generation_seed
|
|
162
|
+
attr_reader :overrides, :traits, :transforms
|
|
163
|
+
|
|
164
|
+
def initialize(name)
|
|
165
|
+
@name = name
|
|
166
|
+
@generation_mode = :random
|
|
167
|
+
@generation_seed = nil
|
|
168
|
+
@overrides = {}
|
|
169
|
+
@traits = []
|
|
170
|
+
@transforms = []
|
|
171
|
+
@before_hooks = []
|
|
172
|
+
@after_hooks = []
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Set generation mode
|
|
176
|
+
#
|
|
177
|
+
# @param mode_name [Symbol] :random, :edge, :invalid, :hostile, :mixed
|
|
178
|
+
#
|
|
179
|
+
def mode(mode_name)
|
|
180
|
+
@generation_mode = mode_name
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Set random seed
|
|
184
|
+
#
|
|
185
|
+
# @param value [Integer] seed value
|
|
186
|
+
#
|
|
187
|
+
def seed(value)
|
|
188
|
+
@generation_seed = value
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Add overrides for a schema
|
|
192
|
+
#
|
|
193
|
+
# @param schema [String, Symbol, Class] schema name or class
|
|
194
|
+
# @param fields [Hash] field overrides
|
|
195
|
+
#
|
|
196
|
+
def override(schema, **fields)
|
|
197
|
+
schema_name = normalize_schema_name(schema)
|
|
198
|
+
@overrides[schema_name] ||= {}
|
|
199
|
+
@overrides[schema_name].merge!(fields)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Include a trait
|
|
203
|
+
#
|
|
204
|
+
# @param trait_name [Symbol] trait to include
|
|
205
|
+
#
|
|
206
|
+
def include_trait(trait_name)
|
|
207
|
+
@traits << trait_name
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Add a transform function
|
|
211
|
+
#
|
|
212
|
+
# @param schema [String, Symbol] schema name (nil for all)
|
|
213
|
+
# @yield [data, context] transform block
|
|
214
|
+
#
|
|
215
|
+
def transform(schema = nil, &block)
|
|
216
|
+
@transforms << { schema: schema&.to_s, block: block }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Add before hook
|
|
220
|
+
def before(&block)
|
|
221
|
+
@before_hooks << block
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Add after hook
|
|
225
|
+
def after(&block)
|
|
226
|
+
@after_hooks << block
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Apply this persona and execute block
|
|
230
|
+
#
|
|
231
|
+
# @yield block for generation
|
|
232
|
+
# @return [Object] result of the block
|
|
233
|
+
#
|
|
234
|
+
def apply(&block)
|
|
235
|
+
context = PersonaContext.new(self)
|
|
236
|
+
context.apply(&block)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Get effective overrides for a schema
|
|
240
|
+
#
|
|
241
|
+
# @param schema_name [String] schema name
|
|
242
|
+
# @return [Hash] merged overrides
|
|
243
|
+
#
|
|
244
|
+
def overrides_for(schema_name)
|
|
245
|
+
result = {}
|
|
246
|
+
|
|
247
|
+
# Apply trait overrides first
|
|
248
|
+
@traits.each do |trait_name|
|
|
249
|
+
trait = Traits[trait_name]
|
|
250
|
+
result.merge!(trait.overrides_for(schema_name)) if trait
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Apply persona overrides
|
|
254
|
+
result.merge!(@overrides[schema_name] || {})
|
|
255
|
+
|
|
256
|
+
result
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private
|
|
260
|
+
|
|
261
|
+
def normalize_schema_name(schema)
|
|
262
|
+
case schema
|
|
263
|
+
when String then schema
|
|
264
|
+
when Symbol then schema.to_s
|
|
265
|
+
when Class then schema.name
|
|
266
|
+
else schema.to_s
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Runtime context for persona application
|
|
272
|
+
class PersonaContext
|
|
273
|
+
attr_reader :persona
|
|
274
|
+
|
|
275
|
+
def initialize(persona)
|
|
276
|
+
@persona = persona
|
|
277
|
+
@generated = {}
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def apply(&block)
|
|
281
|
+
# Run before hooks
|
|
282
|
+
@persona.instance_variable_get(:@before_hooks).each { |h| instance_exec(&h) }
|
|
283
|
+
|
|
284
|
+
# Set thread-local context
|
|
285
|
+
old_context = Thread.current[:synthra_persona]
|
|
286
|
+
Thread.current[:synthra_persona] = self
|
|
287
|
+
|
|
288
|
+
# Yield self to the block instead of instance_eval to preserve caller's context
|
|
289
|
+
result = block.call(self)
|
|
290
|
+
|
|
291
|
+
# Run after hooks
|
|
292
|
+
@persona.instance_variable_get(:@after_hooks).each { |h| instance_exec(&h) }
|
|
293
|
+
|
|
294
|
+
result
|
|
295
|
+
ensure
|
|
296
|
+
Thread.current[:synthra_persona] = old_context
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Generate data with persona settings
|
|
300
|
+
def generate(schema_name, **options)
|
|
301
|
+
schema = Synthra.registry.schema(schema_name.to_s)
|
|
302
|
+
|
|
303
|
+
merged_options = {
|
|
304
|
+
mode: @persona.generation_mode,
|
|
305
|
+
seed: @persona.generation_seed,
|
|
306
|
+
overrides: @persona.overrides_for(schema_name.to_s)
|
|
307
|
+
}.merge(options)
|
|
308
|
+
|
|
309
|
+
data = schema.generate(**merged_options)
|
|
310
|
+
|
|
311
|
+
# Apply transforms
|
|
312
|
+
@persona.transforms.each do |transform|
|
|
313
|
+
if transform[:schema].nil? || transform[:schema] == schema_name.to_s
|
|
314
|
+
data = transform[:block].call(data, self)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
@generated[schema_name.to_s] = data
|
|
319
|
+
data
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Reusable traits for personas
|
|
325
|
+
module Traits
|
|
326
|
+
class << self
|
|
327
|
+
def registry
|
|
328
|
+
@registry ||= {}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Define a trait
|
|
332
|
+
#
|
|
333
|
+
# @param name [Symbol] trait name
|
|
334
|
+
# @yield trait configuration
|
|
335
|
+
#
|
|
336
|
+
def define(name, &block)
|
|
337
|
+
trait = Trait.new(name)
|
|
338
|
+
trait.instance_eval(&block) if block
|
|
339
|
+
registry[name] = trait
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def [](name)
|
|
343
|
+
registry[name]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def reset!
|
|
347
|
+
@registry = {}
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
class Trait
|
|
352
|
+
attr_reader :name, :overrides
|
|
353
|
+
|
|
354
|
+
def initialize(name)
|
|
355
|
+
@name = name
|
|
356
|
+
@overrides = {}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def override(schema, **fields)
|
|
360
|
+
schema_name = schema.to_s
|
|
361
|
+
@overrides[schema_name] ||= {}
|
|
362
|
+
@overrides[schema_name].merge!(fields)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def overrides_for(schema_name)
|
|
366
|
+
@overrides[schema_name] || {}
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Extend main module with persona support
|
|
372
|
+
class << self
|
|
373
|
+
# Generate data using a persona
|
|
374
|
+
#
|
|
375
|
+
# @param persona_name [Symbol] persona to use
|
|
376
|
+
# @yield block for generation
|
|
377
|
+
# @return [Object] result
|
|
378
|
+
#
|
|
379
|
+
# @example
|
|
380
|
+
# Synthra.as(:happy_path) { generate("User") }
|
|
381
|
+
#
|
|
382
|
+
def as(persona_name, &block)
|
|
383
|
+
Personas.as(persona_name, &block)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Get current persona context
|
|
387
|
+
#
|
|
388
|
+
# @return [Personas::PersonaContext, nil]
|
|
389
|
+
#
|
|
390
|
+
def current_persona
|
|
391
|
+
Thread.current[:synthra_persona]
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Initialize built-in personas
|
|
396
|
+
Personas.define_built_in_personas!
|
|
397
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Synthra Property-Based Testing Integration
|
|
5
|
+
# =============================================================================
|
|
6
|
+
#
|
|
7
|
+
# Provides RSpec helpers for property-based testing with Synthra schemas.
|
|
8
|
+
# Allows you to verify properties hold for all generated data.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic property test
|
|
11
|
+
# RSpec.describe "User Registration" do
|
|
12
|
+
# include Synthra::PropertyTesting
|
|
13
|
+
#
|
|
14
|
+
# it "accepts all valid users" do
|
|
15
|
+
# verify_property("User") do |user_data|
|
|
16
|
+
# user = User.create(user_data)
|
|
17
|
+
# expect(user).to be_valid
|
|
18
|
+
# expect(user.save).to be_truthy
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example With custom count and seed
|
|
24
|
+
# verify_property("Order", count: 1000, seed: 42) do |order_data|
|
|
25
|
+
# expect(order_data["total"]).to be >= 0
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
module Synthra
|
|
31
|
+
module PropertyTesting
|
|
32
|
+
# Verify a property holds for generated data
|
|
33
|
+
#
|
|
34
|
+
# Generates multiple records from a schema and verifies a property
|
|
35
|
+
# holds for all of them. Raises if property fails for any record.
|
|
36
|
+
#
|
|
37
|
+
# @param schema_name [String] schema name to generate from
|
|
38
|
+
# @param count [Integer] number of records to generate (default: 100)
|
|
39
|
+
# @param seed [Integer, nil] random seed for reproducibility
|
|
40
|
+
# @param mode [Symbol] generation mode (default: :random)
|
|
41
|
+
# @param registry [Registry, nil] registry for schema lookup
|
|
42
|
+
# @yield [Hash] block to verify property
|
|
43
|
+
# @yieldparam record [Hash] generated record
|
|
44
|
+
# @raise [RSpec::Expectations::ExpectationNotMetError] if property fails
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# verify_property("User") do |user|
|
|
48
|
+
# expect(user["email"]).to match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
def verify_property(schema_name, count: 100, seed: nil, mode: :random, registry: nil)
|
|
52
|
+
registry ||= Registry.new
|
|
53
|
+
schema = registry.schema(schema_name)
|
|
54
|
+
|
|
55
|
+
records = schema.generate_many(count, seed: seed, mode: mode, registry: registry)
|
|
56
|
+
|
|
57
|
+
records.each_with_index do |record, idx|
|
|
58
|
+
begin
|
|
59
|
+
yield(record)
|
|
60
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
|
61
|
+
raise RSpec::Expectations::ExpectationNotMetError,
|
|
62
|
+
"Property failed for record #{idx + 1}/#{count} (seed: #{seed || 'random'}):\n" \
|
|
63
|
+
"#{e.message}\n" \
|
|
64
|
+
"Record: #{record.inspect}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Verify a property holds for a stream of generated data
|
|
70
|
+
#
|
|
71
|
+
# Generates records lazily and verifies property for each.
|
|
72
|
+
# More memory-efficient for large counts.
|
|
73
|
+
#
|
|
74
|
+
# @param schema_name [String] schema name to generate from
|
|
75
|
+
# @param count [Integer] number of records to generate
|
|
76
|
+
# @param seed [Integer, nil] random seed
|
|
77
|
+
# @param mode [Symbol] generation mode
|
|
78
|
+
# @param registry [Registry, nil] registry for schema lookup
|
|
79
|
+
# @yield [Hash] block to verify property
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# verify_property_stream("Order", count: 10000) do |order|
|
|
83
|
+
# expect(order["items"]).to be_an(Array)
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
def verify_property_stream(schema_name, count: 1000, seed: nil, mode: :random, registry: nil)
|
|
87
|
+
registry ||= Registry.new
|
|
88
|
+
schema = registry.schema(schema_name)
|
|
89
|
+
|
|
90
|
+
stream = schema.generate_many_stream(count, seed: seed, mode: mode, registry: registry)
|
|
91
|
+
|
|
92
|
+
stream.each_with_index do |record, idx|
|
|
93
|
+
begin
|
|
94
|
+
yield(record)
|
|
95
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
|
96
|
+
raise RSpec::Expectations::ExpectationNotMetError,
|
|
97
|
+
"Property failed for record #{idx + 1}/#{count} (seed: #{seed || 'random'}):\n" \
|
|
98
|
+
"#{e.message}\n" \
|
|
99
|
+
"Record: #{record.inspect}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Shrink a failing test case to find minimal counterexample
|
|
105
|
+
#
|
|
106
|
+
# When a property fails, attempts to shrink the failing record
|
|
107
|
+
# to find a smaller counterexample.
|
|
108
|
+
#
|
|
109
|
+
# @param schema_name [String] schema name
|
|
110
|
+
# @param failing_record [Hash] record that caused failure
|
|
111
|
+
# @param registry [Registry, nil] registry
|
|
112
|
+
# @yield [Hash] block to verify property
|
|
113
|
+
# @return [Hash] minimal counterexample
|
|
114
|
+
#
|
|
115
|
+
def shrink_counterexample(schema_name, failing_record, registry: nil, &block)
|
|
116
|
+
registry ||= Registry.new
|
|
117
|
+
schema = registry.schema(schema_name)
|
|
118
|
+
|
|
119
|
+
current = failing_record.dup
|
|
120
|
+
improved = true
|
|
121
|
+
|
|
122
|
+
while improved
|
|
123
|
+
improved = false
|
|
124
|
+
|
|
125
|
+
# Try removing optional fields
|
|
126
|
+
current.keys.each do |key|
|
|
127
|
+
next unless schema.field(key)&.optional?
|
|
128
|
+
|
|
129
|
+
candidate = current.dup
|
|
130
|
+
candidate.delete(key)
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
block.call(candidate)
|
|
134
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
|
135
|
+
# Still fails with smaller record
|
|
136
|
+
current = candidate
|
|
137
|
+
improved = true
|
|
138
|
+
break
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Try simplifying array values
|
|
143
|
+
current.each do |key, value|
|
|
144
|
+
next unless value.is_a?(Array) && value.length > 1
|
|
145
|
+
|
|
146
|
+
candidate = current.dup
|
|
147
|
+
candidate[key] = value[0..0] # Single element
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
block.call(candidate)
|
|
151
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
|
152
|
+
current = candidate
|
|
153
|
+
improved = true
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
current
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Include in RSpec
|
|
165
|
+
# :nocov: — load-time RSpec wiring. This file is only required while RSpec is
|
|
166
|
+
# loaded, so `defined?(RSpec)` is always truthy here; the false arm is unreachable.
|
|
167
|
+
if defined?(RSpec)
|
|
168
|
+
RSpec.configure do |config|
|
|
169
|
+
config.include Synthra::PropertyTesting
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
# :nocov:
|