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,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# Factory Bot Integration
|
|
5
|
+
#
|
|
6
|
+
# Provides seamless integration between Synthra and Factory Bot.
|
|
7
|
+
# Define factories using Synthra schemas with full trait support.
|
|
8
|
+
#
|
|
9
|
+
# @example Define a factory
|
|
10
|
+
# Synthra.define_factory(:user, schema: "User") do
|
|
11
|
+
# trait(:admin) do
|
|
12
|
+
# role { "admin" }
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# trait(:with_address) do
|
|
16
|
+
# address { Synthra.generate("Address") }
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Use in tests
|
|
21
|
+
# create(:user)
|
|
22
|
+
# create(:user, :admin)
|
|
23
|
+
# create(:user, :admin, :with_address, name: "Custom Name")
|
|
24
|
+
#
|
|
25
|
+
module FactoryBotIntegration
|
|
26
|
+
class << self
|
|
27
|
+
# Registry of Synthra factories
|
|
28
|
+
attr_accessor :factories
|
|
29
|
+
|
|
30
|
+
# Define a factory using a Synthra schema
|
|
31
|
+
#
|
|
32
|
+
# @param factory_name [Symbol] the factory name
|
|
33
|
+
# @param schema [String] the schema name
|
|
34
|
+
# @yield block for defining traits and overrides
|
|
35
|
+
# @return [void]
|
|
36
|
+
#
|
|
37
|
+
def define_factory(factory_name, schema:, &block)
|
|
38
|
+
@factories ||= {}
|
|
39
|
+
@factories[factory_name] = {
|
|
40
|
+
schema: schema,
|
|
41
|
+
traits: {},
|
|
42
|
+
overrides: {},
|
|
43
|
+
block: block
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Register with Factory Bot if available
|
|
47
|
+
register_with_factory_bot(factory_name, schema, &block) if factory_bot_available?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Generate data using a defined factory
|
|
51
|
+
#
|
|
52
|
+
# @param factory_name [Symbol] the factory name
|
|
53
|
+
# @param traits [Array<Symbol>] traits to apply
|
|
54
|
+
# @param overrides [Hash] field overrides
|
|
55
|
+
# @return [Hash] generated data
|
|
56
|
+
#
|
|
57
|
+
def build(factory_name, *traits, **overrides)
|
|
58
|
+
factory = @factories&.[](factory_name)
|
|
59
|
+
raise ArgumentError, "Unknown factory: #{factory_name}" unless factory
|
|
60
|
+
|
|
61
|
+
# Get the schema
|
|
62
|
+
schema = Synthra.parse(load_schema_source(factory[:schema]))
|
|
63
|
+
|
|
64
|
+
# Apply traits
|
|
65
|
+
merged_overrides = overrides.dup
|
|
66
|
+
traits.each do |trait_name|
|
|
67
|
+
trait = factory[:traits][trait_name]
|
|
68
|
+
merged_overrides.merge!(trait) if trait
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Generate with overrides
|
|
72
|
+
schema.generate(overrides: merged_overrides)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if Factory Bot is available
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
#
|
|
79
|
+
def factory_bot_available?
|
|
80
|
+
!!defined?(FactoryBot)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def load_schema_source(schema_name)
|
|
86
|
+
# Check if it's already a schema object or a string
|
|
87
|
+
if schema_name.is_a?(Schema)
|
|
88
|
+
return schema_name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Try to find in default registry
|
|
92
|
+
if Synthra.instance_variable_get(:@default_registry)
|
|
93
|
+
registry = Synthra.instance_variable_get(:@default_registry)
|
|
94
|
+
return registry.schema(schema_name) if registry.schema?(schema_name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Create minimal DSL for the schema
|
|
98
|
+
"#{schema_name}:\n id: uuid"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :nocov: requires the factory_bot gem's DSL runtime (FactoryBot.define / factory / trait),
|
|
102
|
+
# which is not a dependency of this gem; exercised only inside a consuming app that has
|
|
103
|
+
# factory_bot loaded. The dispatch into here is covered (see define_factory spec).
|
|
104
|
+
def register_with_factory_bot(factory_name, schema_name, &block)
|
|
105
|
+
# Integration with Factory Bot DSL
|
|
106
|
+
factory_config = @factories[factory_name]
|
|
107
|
+
|
|
108
|
+
FactoryBot.define do
|
|
109
|
+
factory factory_name do
|
|
110
|
+
initialize_with do
|
|
111
|
+
Synthra::FactoryBotIntegration.build(
|
|
112
|
+
factory_name,
|
|
113
|
+
*@build_strategy_traits,
|
|
114
|
+
**attributes
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Process the block to extract traits
|
|
119
|
+
instance_exec(&block) if block
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
# :nocov:
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# DSL methods available inside factory definition blocks
|
|
127
|
+
module DSL
|
|
128
|
+
# Define a trait for the factory
|
|
129
|
+
#
|
|
130
|
+
# @param name [Symbol] trait name
|
|
131
|
+
# @yield block that returns field overrides
|
|
132
|
+
#
|
|
133
|
+
def trait(name, &block)
|
|
134
|
+
# Store the trait for later use
|
|
135
|
+
@current_factory_traits ||= {}
|
|
136
|
+
@current_factory_traits[name] = block
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# RSpec integration helpers
|
|
141
|
+
module RSpec
|
|
142
|
+
# Include Synthra factory support in RSpec
|
|
143
|
+
#
|
|
144
|
+
# @example In spec_helper.rb
|
|
145
|
+
# require 'synthra/factory_bot_integration'
|
|
146
|
+
# RSpec.configure do |config|
|
|
147
|
+
# config.include Synthra::FactoryBotIntegration::RSpec
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
def fake_data_build(factory_name, *traits, **overrides)
|
|
151
|
+
FactoryBotIntegration.build(factory_name, *traits, **overrides)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
alias build_fake_data fake_data_build
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# Synthra Field and FieldBehavior Classes
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# This file defines the Field class which represents a single field in a
|
|
9
|
+
# schema, and the FieldBehavior class for field-level behaviors.
|
|
10
|
+
#
|
|
11
|
+
# Fields are created during DSL parsing and contain all information needed
|
|
12
|
+
# to generate values: type, arguments, modifiers, and behaviors.
|
|
13
|
+
#
|
|
14
|
+
# @example Field in DSL
|
|
15
|
+
# User:
|
|
16
|
+
# name: text # Simple field
|
|
17
|
+
# age: number(18..80) # Field with arguments
|
|
18
|
+
# email?: email # Optional field (? suffix)
|
|
19
|
+
# status: enum(active, inactive) @partial_data 20% # Field with behavior
|
|
20
|
+
#
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
module Synthra
|
|
25
|
+
|
|
26
|
+
# Represents a single field in a schema definition
|
|
27
|
+
#
|
|
28
|
+
# A Field encapsulates all information about a schema field including:
|
|
29
|
+
# - Field name and whether it's optional
|
|
30
|
+
# - Type name and arguments for generation
|
|
31
|
+
# - Nullable status (can the value be nil?)
|
|
32
|
+
# - Conditional expression (when should this field be included?)
|
|
33
|
+
# - Field-level behaviors (latency, partial_data, etc.)
|
|
34
|
+
#
|
|
35
|
+
# @example Create a field programmatically
|
|
36
|
+
# field = Field.new(
|
|
37
|
+
# name: "email",
|
|
38
|
+
# type_name: "email",
|
|
39
|
+
# type_args: { domain: "example.com" },
|
|
40
|
+
# nullable: false
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# @example Optional field (from DSL: "nickname?: text")
|
|
44
|
+
# field = Field.new(name: "nickname?", type_name: "text")
|
|
45
|
+
# field.name # => "nickname"
|
|
46
|
+
# field.optional? # => true
|
|
47
|
+
#
|
|
48
|
+
# @example Nullable type (from DSL: "manager: User?")
|
|
49
|
+
# field = Field.new(name: "manager", type_name: "User?")
|
|
50
|
+
# field.nullable? # => true
|
|
51
|
+
#
|
|
52
|
+
|
|
53
|
+
class Field
|
|
54
|
+
|
|
55
|
+
# @!attribute [r] name
|
|
56
|
+
# The field name (without ? suffix)
|
|
57
|
+
# @return [String] the clean field name
|
|
58
|
+
|
|
59
|
+
attr_reader :name
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# @!attribute [r] type_name
|
|
63
|
+
# The type name (without ? suffix)
|
|
64
|
+
# @return [String] the clean type name
|
|
65
|
+
|
|
66
|
+
attr_reader :type_name
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# @!attribute [r] type_args
|
|
70
|
+
# Arguments passed to the type generator
|
|
71
|
+
# @return [Hash] type arguments (e.g., { min: 0, max: 100 })
|
|
72
|
+
|
|
73
|
+
attr_reader :type_args
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# @!attribute [r] condition
|
|
77
|
+
# Field name for conditional inclusion
|
|
78
|
+
# @return [String, nil] the condition field name, or nil if unconditional
|
|
79
|
+
|
|
80
|
+
attr_reader :condition
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# @!attribute [r] behaviors
|
|
84
|
+
# Field-level behaviors (latency, partial_data, etc.)
|
|
85
|
+
# @return [Array<FieldBehavior>] list of field behaviors
|
|
86
|
+
|
|
87
|
+
attr_reader :behaviors
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Create a new Field instance
|
|
91
|
+
#
|
|
92
|
+
# @param name [String] field name (may include ? suffix for optional)
|
|
93
|
+
# @param type_name [String] the type name (may include ? suffix for nullable)
|
|
94
|
+
# @param type_args [Hash] arguments to pass to the type generator
|
|
95
|
+
# @param nullable [Boolean] whether the field value can be nil
|
|
96
|
+
# @param condition [String, nil] field name that must be truthy for inclusion
|
|
97
|
+
# @param behaviors [Array<FieldBehavior>] field-level behaviors to apply
|
|
98
|
+
#
|
|
99
|
+
# @example Simple field
|
|
100
|
+
# Field.new(name: "id", type_name: "uuid")
|
|
101
|
+
#
|
|
102
|
+
# @example Field with arguments
|
|
103
|
+
# Field.new(
|
|
104
|
+
# name: "age",
|
|
105
|
+
# type_name: "number",
|
|
106
|
+
# type_args: { min: 18, max: 80 }
|
|
107
|
+
# )
|
|
108
|
+
#
|
|
109
|
+
# @example Optional nullable field with condition
|
|
110
|
+
# Field.new(
|
|
111
|
+
# name: "manager?",
|
|
112
|
+
# type_name: "User?",
|
|
113
|
+
# condition: "is_employee"
|
|
114
|
+
# )
|
|
115
|
+
#
|
|
116
|
+
|
|
117
|
+
def initialize(name:, type_name:, type_args: {}, nullable: false, condition: nil, behaviors: [])
|
|
118
|
+
@raw_name = name
|
|
119
|
+
@name = name.delete_suffix("?")
|
|
120
|
+
@optional = name.end_with?("?")
|
|
121
|
+
@type_name = type_name.to_s.delete_suffix("?")
|
|
122
|
+
@type_args = type_args || {}
|
|
123
|
+
@nullable = nullable || type_name.to_s.end_with?("?")
|
|
124
|
+
@condition = condition
|
|
125
|
+
@behaviors = behaviors
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Check if this field is optional
|
|
130
|
+
#
|
|
131
|
+
# Optional fields (marked with ? suffix in DSL) may be omitted from
|
|
132
|
+
# the generated output entirely based on configuration or randomness.
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] true if the field is optional
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# Field.new(name: "nickname?", type_name: "text").optional? # => true
|
|
138
|
+
# Field.new(name: "name", type_name: "text").optional? # => false
|
|
139
|
+
#
|
|
140
|
+
|
|
141
|
+
def optional?
|
|
142
|
+
@optional
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Check if this field's type is nullable
|
|
147
|
+
#
|
|
148
|
+
# Nullable types (marked with ? suffix on type in DSL) can generate
|
|
149
|
+
# nil values. This is different from optional - nullable fields are
|
|
150
|
+
# always included but may have nil values.
|
|
151
|
+
#
|
|
152
|
+
# @return [Boolean] true if the type can be nil
|
|
153
|
+
#
|
|
154
|
+
# @example
|
|
155
|
+
# Field.new(name: "manager", type_name: "User?").nullable? # => true
|
|
156
|
+
# Field.new(name: "manager", type_name: "User").nullable? # => false
|
|
157
|
+
#
|
|
158
|
+
|
|
159
|
+
def nullable?
|
|
160
|
+
@nullable
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Check if this field has a condition
|
|
165
|
+
#
|
|
166
|
+
# Conditional fields are only included when another field has a truthy
|
|
167
|
+
# value. The condition is specified with "if field_name" in the DSL.
|
|
168
|
+
#
|
|
169
|
+
# @return [Boolean] true if the field has a condition
|
|
170
|
+
#
|
|
171
|
+
# @example DSL: "admin_note: text if is_admin"
|
|
172
|
+
# field.conditional? # => true
|
|
173
|
+
# field.condition # => "is_admin"
|
|
174
|
+
#
|
|
175
|
+
|
|
176
|
+
def conditional?
|
|
177
|
+
!condition.nil?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Check if this field has a uniqueness constraint
|
|
182
|
+
#
|
|
183
|
+
# Unique fields will not generate duplicate values within a batch.
|
|
184
|
+
# The generator will retry up to max_unique_retries times.
|
|
185
|
+
#
|
|
186
|
+
# @return [Boolean] true if the field requires unique values
|
|
187
|
+
#
|
|
188
|
+
# @example DSL: "id: uuid unique"
|
|
189
|
+
# field.unique? # => true
|
|
190
|
+
#
|
|
191
|
+
|
|
192
|
+
def unique?
|
|
193
|
+
type_args[:unique] == true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Get the range constraint if present
|
|
198
|
+
#
|
|
199
|
+
# Range constraints limit the generated values to a specific range.
|
|
200
|
+
# Common for number types.
|
|
201
|
+
#
|
|
202
|
+
# @return [Range, nil] the range constraint, or nil if not specified
|
|
203
|
+
#
|
|
204
|
+
# @example DSL: "age: number(18..80)"
|
|
205
|
+
# field.range # => 18..80
|
|
206
|
+
#
|
|
207
|
+
|
|
208
|
+
def range
|
|
209
|
+
return nil unless type_args[:range]
|
|
210
|
+
|
|
211
|
+
type_args[:range]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Check if this field has a specific behavior
|
|
216
|
+
#
|
|
217
|
+
# @param type [Symbol, String] the behavior type to check for
|
|
218
|
+
# @return [Boolean] true if the field has the specified behavior
|
|
219
|
+
#
|
|
220
|
+
# @example
|
|
221
|
+
# field.has_behavior?(:latency) # => true
|
|
222
|
+
# field.has_behavior?(:partial_data) # => false
|
|
223
|
+
#
|
|
224
|
+
|
|
225
|
+
def has_behavior?(type)
|
|
226
|
+
behaviors.any? { |b| b.type == type.to_sym }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Get a specific behavior by type
|
|
231
|
+
#
|
|
232
|
+
# @param type [Symbol, String] the behavior type to retrieve
|
|
233
|
+
# @return [FieldBehavior, nil] the behavior or nil if not found
|
|
234
|
+
#
|
|
235
|
+
# @example
|
|
236
|
+
# behavior = field.behavior(:latency)
|
|
237
|
+
# behavior.value # => { min: 100, max: 500 }
|
|
238
|
+
#
|
|
239
|
+
|
|
240
|
+
def behavior(type)
|
|
241
|
+
behaviors.find { |b| b.type == type.to_sym }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Get all modifiers as a hash
|
|
246
|
+
#
|
|
247
|
+
# Returns a hash containing all field modifiers (unique, range,
|
|
248
|
+
# nullable, optional) with their values, excluding nil values.
|
|
249
|
+
#
|
|
250
|
+
# @return [Hash] modifier name => value pairs
|
|
251
|
+
#
|
|
252
|
+
# @example
|
|
253
|
+
# field.modifiers
|
|
254
|
+
# # => { unique: true, nullable: false, optional: true }
|
|
255
|
+
#
|
|
256
|
+
|
|
257
|
+
def modifiers
|
|
258
|
+
{
|
|
259
|
+
unique: unique?,
|
|
260
|
+
range: range,
|
|
261
|
+
nullable: nullable?,
|
|
262
|
+
optional: optional?
|
|
263
|
+
}.compact
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# String representation of the field
|
|
268
|
+
#
|
|
269
|
+
# Returns a human-readable string showing the field definition
|
|
270
|
+
# similar to how it would appear in a DSL file.
|
|
271
|
+
#
|
|
272
|
+
# @return [String] string representation
|
|
273
|
+
#
|
|
274
|
+
# @example
|
|
275
|
+
# field.to_s # => "name: text"
|
|
276
|
+
# field.to_s # => "age: number({min: 18, max: 80})"
|
|
277
|
+
#
|
|
278
|
+
|
|
279
|
+
def to_s
|
|
280
|
+
parts = ["#{@raw_name}: #{type_name}"]
|
|
281
|
+
parts << "(#{type_args})" unless type_args.empty?
|
|
282
|
+
parts << "if #{condition}" if condition
|
|
283
|
+
behaviors.each { |b| parts << "@#{b.type} #{b.value}" }
|
|
284
|
+
parts.join(" ")
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# Inspect representation for debugging
|
|
289
|
+
#
|
|
290
|
+
# @return [String] inspect string
|
|
291
|
+
#
|
|
292
|
+
|
|
293
|
+
def inspect
|
|
294
|
+
"#<Field #{self}>"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# Represents a field-level behavior
|
|
300
|
+
#
|
|
301
|
+
# Field behaviors modify how individual fields are generated or returned.
|
|
302
|
+
# They can add latency, omit data, or apply other transformations to
|
|
303
|
+
# specific fields rather than the entire schema.
|
|
304
|
+
#
|
|
305
|
+
# @example DSL field with behavior
|
|
306
|
+
# # User:
|
|
307
|
+
# # profile_image: url @partial_data 30% # 30% chance of null
|
|
308
|
+
# # bio: text @latency 100..500ms # 100-500ms delay
|
|
309
|
+
#
|
|
310
|
+
# @example Create a field behavior programmatically
|
|
311
|
+
# behavior = FieldBehavior.new(
|
|
312
|
+
# type: :latency,
|
|
313
|
+
# value: { min: 100, max: 500 }
|
|
314
|
+
# )
|
|
315
|
+
#
|
|
316
|
+
|
|
317
|
+
class FieldBehavior
|
|
318
|
+
|
|
319
|
+
# @!attribute [r] type
|
|
320
|
+
# The behavior type (:latency, :partial_data, etc.)
|
|
321
|
+
# @return [Symbol] behavior type
|
|
322
|
+
|
|
323
|
+
attr_reader :type
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# @!attribute [r] value
|
|
327
|
+
# The behavior configuration value
|
|
328
|
+
# @return [Integer, Range, Hash] behavior value/configuration
|
|
329
|
+
|
|
330
|
+
attr_reader :value
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# Create a new FieldBehavior
|
|
334
|
+
#
|
|
335
|
+
# @param type [Symbol, String] behavior type (:partial_data, :latency, etc.)
|
|
336
|
+
# @param value [Integer, Range, Hash] behavior configuration
|
|
337
|
+
#
|
|
338
|
+
# @example Percentage-based behavior
|
|
339
|
+
# FieldBehavior.new(type: :partial_data, value: 30)
|
|
340
|
+
#
|
|
341
|
+
# @example Range-based behavior
|
|
342
|
+
# FieldBehavior.new(type: :latency, value: { min: 100, max: 500 })
|
|
343
|
+
#
|
|
344
|
+
|
|
345
|
+
def initialize(type:, value:)
|
|
346
|
+
@type = type.to_sym
|
|
347
|
+
@value = value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Get the behavior value as a percentage
|
|
352
|
+
#
|
|
353
|
+
# For probability-based behaviors, extracts the percentage value
|
|
354
|
+
# from various value formats.
|
|
355
|
+
#
|
|
356
|
+
# @return [Integer] the percentage (0-100)
|
|
357
|
+
#
|
|
358
|
+
# @example
|
|
359
|
+
# FieldBehavior.new(type: :partial_data, value: 30).percentage
|
|
360
|
+
# # => 30
|
|
361
|
+
#
|
|
362
|
+
# FieldBehavior.new(type: :partial_data, value: { probability: 25 }).percentage
|
|
363
|
+
# # => 25
|
|
364
|
+
#
|
|
365
|
+
|
|
366
|
+
def percentage
|
|
367
|
+
case value
|
|
368
|
+
when Integer then value
|
|
369
|
+
when Hash then value[:percent] || value[:probability] || 0
|
|
370
|
+
else 0
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# Get the behavior value as a range
|
|
376
|
+
#
|
|
377
|
+
# For range-based behaviors (like latency), converts various value
|
|
378
|
+
# formats into a Range object.
|
|
379
|
+
#
|
|
380
|
+
# @return [Range] the range
|
|
381
|
+
#
|
|
382
|
+
# @example
|
|
383
|
+
# FieldBehavior.new(type: :latency, value: 100..500).range
|
|
384
|
+
# # => 100..500
|
|
385
|
+
#
|
|
386
|
+
# FieldBehavior.new(type: :latency, value: { min: 100, max: 500 }).range
|
|
387
|
+
# # => 100..500
|
|
388
|
+
#
|
|
389
|
+
# FieldBehavior.new(type: :latency, value: 100).range
|
|
390
|
+
# # => 100..100
|
|
391
|
+
#
|
|
392
|
+
|
|
393
|
+
def range
|
|
394
|
+
case value
|
|
395
|
+
when Range then value
|
|
396
|
+
when Hash
|
|
397
|
+
min = value[:min] || 0
|
|
398
|
+
max = value[:max] || min
|
|
399
|
+
min..max
|
|
400
|
+
else
|
|
401
|
+
value..value
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# String representation
|
|
407
|
+
#
|
|
408
|
+
# @return [String] string like "@latency 100..500"
|
|
409
|
+
#
|
|
410
|
+
|
|
411
|
+
def to_s
|
|
412
|
+
"@#{type} #{value}"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Inspect representation
|
|
417
|
+
#
|
|
418
|
+
# @return [String] inspect string
|
|
419
|
+
#
|
|
420
|
+
|
|
421
|
+
def inspect
|
|
422
|
+
"#<FieldBehavior #{self}>"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# Check equality with another FieldBehavior
|
|
427
|
+
#
|
|
428
|
+
# Two behaviors are equal if they have the same type and value.
|
|
429
|
+
#
|
|
430
|
+
# @param other [FieldBehavior] the other behavior to compare
|
|
431
|
+
# @return [Boolean] true if equal
|
|
432
|
+
#
|
|
433
|
+
|
|
434
|
+
def ==(other)
|
|
435
|
+
return false unless other.is_a?(FieldBehavior)
|
|
436
|
+
|
|
437
|
+
type == other.type && value == other.value
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Functions
|
|
5
|
+
|
|
6
|
+
# Registry for custom functions used in computed fields
|
|
7
|
+
class Registry
|
|
8
|
+
@functions = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
|
|
13
|
+
# Register a custom function
|
|
14
|
+
#
|
|
15
|
+
# @param name [Symbol] function name
|
|
16
|
+
# @param force [Boolean] allow overwriting existing function
|
|
17
|
+
# @yield [context] block that computes the value
|
|
18
|
+
# @return [void]
|
|
19
|
+
# @raise [ArgumentError] if function exists and force is false
|
|
20
|
+
|
|
21
|
+
def register(name, force: false, &block)
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
key = name.to_sym
|
|
24
|
+
if @functions.key?(key) && !force
|
|
25
|
+
raise ArgumentError, "Function '#{name}' already registered. Use force: true to override."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@functions[key] = block
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Lookup a registered function
|
|
34
|
+
#
|
|
35
|
+
# @param name [Symbol] function name
|
|
36
|
+
# @return [Proc]
|
|
37
|
+
# @raise [MissingFunctionError] if not found
|
|
38
|
+
|
|
39
|
+
def lookup(name)
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@functions.fetch(name.to_sym) do
|
|
42
|
+
raise MissingFunctionError, name
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Call a registered function
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol] function name
|
|
51
|
+
# @param context [Context] generation context
|
|
52
|
+
# @return [Object] result
|
|
53
|
+
|
|
54
|
+
def call(name, context)
|
|
55
|
+
func = lookup(name)
|
|
56
|
+
func.call(context)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Check if a function is registered
|
|
61
|
+
#
|
|
62
|
+
# @param name [Symbol] function name
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
|
|
65
|
+
def registered?(name)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@functions.key?(name.to_sym)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# List all registered function names
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<Symbol>]
|
|
75
|
+
|
|
76
|
+
def names
|
|
77
|
+
@mutex.synchronize { @functions.keys }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Clear all registered functions
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
|
|
85
|
+
def clear
|
|
86
|
+
@mutex.synchronize { @functions.clear }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Unregister a specific function
|
|
91
|
+
#
|
|
92
|
+
# @param name [Symbol] function name
|
|
93
|
+
# @return [Proc, nil] the removed function
|
|
94
|
+
|
|
95
|
+
def unregister(name)
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
@functions.delete(name.to_sym)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|