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,1270 @@
|
|
|
1
|
+
# Native Rust Extension with fake-rs
|
|
2
|
+
|
|
3
|
+
A comprehensive guide to building a high-performance native Rust extension for FakeDataDSL using [fake-rs](https://github.com/cksac/fake-rs).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Why fake-rs?](#why-fake-rs)
|
|
11
|
+
- [Architecture](#architecture)
|
|
12
|
+
- [Performance Comparison](#performance-comparison)
|
|
13
|
+
- [Feature Comparison: Ruby Faker vs fake-rs](#feature-comparison-ruby-faker-vs-fake-rs)
|
|
14
|
+
- [Implementation Guide](#implementation-guide)
|
|
15
|
+
- [API Design](#api-design)
|
|
16
|
+
- [Usage Examples](#usage-examples)
|
|
17
|
+
- [Building and Installation](#building-and-installation)
|
|
18
|
+
- [Troubleshooting](#troubleshooting)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
### The Problem with Calling Ruby Faker from Rust
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
28
|
+
│ ❌ BAD: Rust calling Ruby Faker │
|
|
29
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
30
|
+
│ │
|
|
31
|
+
│ Rust ──► [Acquire GVL] ──► [Ruby VM] ──► [Faker] ──► [Release GVL] ──► │
|
|
32
|
+
│ │
|
|
33
|
+
│ Time: ~25 microseconds per call │
|
|
34
|
+
│ Speed: 40,000 records/sec (SLOWER than pure Ruby!) │
|
|
35
|
+
│ Parallelism: ❌ None (GVL serializes everything) │
|
|
36
|
+
│ │
|
|
37
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### The Solution: Native Rust Faker (fake-rs)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
44
|
+
│ ✅ GOOD: Rust with fake-rs │
|
|
45
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
46
|
+
│ │
|
|
47
|
+
│ Rust ──► [fake-rs] ──► Result │
|
|
48
|
+
│ │
|
|
49
|
+
│ Time: ~100 nanoseconds per call │
|
|
50
|
+
│ Speed: 5,000,000+ records/sec │
|
|
51
|
+
│ Parallelism: ✅ Full (no GVL, use all CPU cores) │
|
|
52
|
+
│ │
|
|
53
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why fake-rs?
|
|
59
|
+
|
|
60
|
+
[fake-rs](https://github.com/cksac/fake-rs) is a mature Rust library for generating fake data with:
|
|
61
|
+
|
|
62
|
+
### Features
|
|
63
|
+
|
|
64
|
+
| Feature | fake-rs | Ruby Faker |
|
|
65
|
+
|---------|---------|------------|
|
|
66
|
+
| Names | ✅ FirstName, LastName, Name, Title | ✅ Same |
|
|
67
|
+
| Email | ✅ FreeEmail, SafeEmail, Username | ✅ Same |
|
|
68
|
+
| Address | ✅ City, Country, Street, ZipCode | ✅ Same |
|
|
69
|
+
| Phone | ✅ PhoneNumber, CellNumber | ✅ Same |
|
|
70
|
+
| Lorem | ✅ Word, Sentence, Paragraph | ✅ Same |
|
|
71
|
+
| Company | ✅ CompanyName, Buzzword, CatchPhrase | ✅ Same |
|
|
72
|
+
| Internet | ✅ IPv4, IPv6, UserAgent, Password | ✅ Same |
|
|
73
|
+
| Date/Time | ✅ Date, Time, DateTime | ✅ Same |
|
|
74
|
+
| UUID | ✅ UUIDv1, UUIDv3, UUIDv4, UUIDv5 | ✅ Same |
|
|
75
|
+
| Finance | ✅ CreditCard, Bic, Isin | ✅ Same |
|
|
76
|
+
| Color | ✅ HexColor, RgbColor, HslColor | ✅ Same |
|
|
77
|
+
| File | ✅ FilePath, FileName, MimeType | ✅ Same |
|
|
78
|
+
| **Locales** | ✅ EN, FR, ZH_TW, ZH_CN, JA_JP, DE, IT, etc. | ✅ Same |
|
|
79
|
+
| **Performance** | ✅ ~100ns per call | ❌ ~20μs per call |
|
|
80
|
+
| **Thread Safety** | ✅ Lock-free | ❌ GVL required |
|
|
81
|
+
|
|
82
|
+
### Supported Locales in fake-rs
|
|
83
|
+
|
|
84
|
+
```rust
|
|
85
|
+
// Available locales in fake-rs
|
|
86
|
+
use fake::locales::*;
|
|
87
|
+
|
|
88
|
+
EN // English (default)
|
|
89
|
+
FR_FR // French
|
|
90
|
+
AR_SA // Arabic
|
|
91
|
+
ZH_TW // Traditional Chinese
|
|
92
|
+
ZH_CN // Simplified Chinese
|
|
93
|
+
JA_JP // Japanese
|
|
94
|
+
PT_BR // Portuguese (Brazilian)
|
|
95
|
+
PT_PT // Portuguese (Portugal)
|
|
96
|
+
DE_DE // German
|
|
97
|
+
IT_IT // Italian
|
|
98
|
+
CY_GB // Welsh
|
|
99
|
+
NL_NL // Dutch
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
108
|
+
│ NATIVE EXTENSION ARCHITECTURE │
|
|
109
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
110
|
+
│ │
|
|
111
|
+
│ Ruby Layer (gem API): │
|
|
112
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
113
|
+
│ │ module FakeDataDSL │ │
|
|
114
|
+
│ │ module Native │ │
|
|
115
|
+
│ │ # Ruby interface to Rust extension │ │
|
|
116
|
+
│ │ def self.generate(schema_spec, count, seed, options) │ │
|
|
117
|
+
│ │ def self.generate_to_file(schema_spec, count, path, options) │ │
|
|
118
|
+
│ │ def self.available_types │ │
|
|
119
|
+
│ │ def self.available_locales │ │
|
|
120
|
+
│ │ end │ │
|
|
121
|
+
│ │ end │ │
|
|
122
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
123
|
+
│ │ │
|
|
124
|
+
│ ▼ │
|
|
125
|
+
│ Rust Extension (native code): │
|
|
126
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
127
|
+
│ │ // Built with magnus (Ruby-Rust bindings) │ │
|
|
128
|
+
│ │ │ │
|
|
129
|
+
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌─────────────────┐ │ │
|
|
130
|
+
│ │ │ Schema Parser │ │ fake-rs Types │ │ Rayon Parallel │ │ │
|
|
131
|
+
│ │ │ (from Ruby) │ │ (native faker) │ │ (multi-core) │ │ │
|
|
132
|
+
│ │ └─────────┬─────────┘ └─────────┬─────────┘ └────────┬────────┘ │ │
|
|
133
|
+
│ │ │ │ │ │ │
|
|
134
|
+
│ │ ▼ ▼ ▼ │ │
|
|
135
|
+
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
|
136
|
+
│ │ │ Generation Engine │ │ │
|
|
137
|
+
│ │ │ - Parse field specs from Ruby │ │ │
|
|
138
|
+
│ │ │ - Map to fake-rs generators │ │ │
|
|
139
|
+
│ │ │ - Generate in parallel using Rayon │ │ │
|
|
140
|
+
│ │ │ - Stream to file or return to Ruby │ │ │
|
|
141
|
+
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
|
142
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
143
|
+
│ │
|
|
144
|
+
│ Dependencies: │
|
|
145
|
+
│ - magnus: Ruby-Rust bindings │
|
|
146
|
+
│ - fake: fake-rs library for generating fake data │
|
|
147
|
+
│ - rayon: Parallel processing │
|
|
148
|
+
│ - serde_json: JSON serialization │
|
|
149
|
+
│ - rand: Random number generation │
|
|
150
|
+
│ │
|
|
151
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Performance Comparison
|
|
157
|
+
|
|
158
|
+
### Benchmarks
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
162
|
+
│ PERFORMANCE BENCHMARKS │
|
|
163
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
164
|
+
│ │
|
|
165
|
+
│ Test: Generate 10 million records with 10 fields │
|
|
166
|
+
│ Hardware: Apple M1 Pro, 16GB RAM │
|
|
167
|
+
│ Schema: uuid, name, email, phone, city, age, active, role, created_at, bio │
|
|
168
|
+
│ │
|
|
169
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
170
|
+
│ │ Engine │ Records/sec │ 10M Time │ Memory │ │
|
|
171
|
+
│ ├─────────────────────────────────────────────────────────────────────┤ │
|
|
172
|
+
│ │ Ruby + Faker │ 20,000 │ 8 min │ 5GB (grows) │ │
|
|
173
|
+
│ │ Ruby Cached Faker │ 300,000 │ 35 sec │ 25MB │ │
|
|
174
|
+
│ │ Rust + fake-rs (1 core) │ 800,000 │ 12 sec │ 10MB │ │
|
|
175
|
+
│ │ Rust + fake-rs (8 cores) │ 5,000,000 │ 2 sec │ 50MB │ │
|
|
176
|
+
│ │ Rust + fake-rs (16 cores) │ 8,000,000 │ 1.2 sec │ 80MB │ │
|
|
177
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
178
|
+
│ │
|
|
179
|
+
│ Speedup vs Ruby Faker: │
|
|
180
|
+
│ - Single core: 40x faster │
|
|
181
|
+
│ - 8 cores: 250x faster │
|
|
182
|
+
│ - 16 cores: 400x faster │
|
|
183
|
+
│ │
|
|
184
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Per-Operation Timing
|
|
188
|
+
|
|
189
|
+
| Operation | Ruby Faker | fake-rs | Speedup |
|
|
190
|
+
|-----------|------------|---------|---------|
|
|
191
|
+
| `Name.name` | 25 μs | 80 ns | 312x |
|
|
192
|
+
| `Internet.email` | 35 μs | 120 ns | 291x |
|
|
193
|
+
| `Address.city` | 15 μs | 50 ns | 300x |
|
|
194
|
+
| `Lorem.paragraph` | 45 μs | 150 ns | 300x |
|
|
195
|
+
| `PhoneNumber.phone_number` | 20 μs | 60 ns | 333x |
|
|
196
|
+
| UUID v4 | 2 μs | 30 ns | 66x |
|
|
197
|
+
| Random number | 0.1 μs | 5 ns | 20x |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Feature Comparison: Ruby Faker vs fake-rs
|
|
202
|
+
|
|
203
|
+
### Type Mapping
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
207
|
+
│ TYPE MAPPING: Ruby Faker → fake-rs │
|
|
208
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
209
|
+
│ │
|
|
210
|
+
│ FakeDataDSL Type Ruby Faker fake-rs │
|
|
211
|
+
│ ───────────────────────────────────────────────────────────────────────── │
|
|
212
|
+
│ │
|
|
213
|
+
│ PERSONAL │
|
|
214
|
+
│ name Faker::Name.name Name(EN) │
|
|
215
|
+
│ first_name Faker::Name.first_name FirstName(EN) │
|
|
216
|
+
│ last_name Faker::Name.last_name LastName(EN) │
|
|
217
|
+
│ full_name Faker::Name.name Name(EN) │
|
|
218
|
+
│ title Faker::Name.prefix Title(EN) │
|
|
219
|
+
│ │
|
|
220
|
+
│ INTERNET │
|
|
221
|
+
│ email Faker::Internet.email FreeEmail(EN) │
|
|
222
|
+
│ username Faker::Internet.username Username(EN) │
|
|
223
|
+
│ password Faker::Internet.password Password(EN, 8..20) │
|
|
224
|
+
│ url Faker::Internet.url (custom) │
|
|
225
|
+
│ ip Faker::Internet.ip_v4_address IPv4() │
|
|
226
|
+
│ ipv6 Faker::Internet.ip_v6_address IPv6() │
|
|
227
|
+
│ mac_address Faker::Internet.mac_address MACAddress() │
|
|
228
|
+
│ user_agent Faker::Internet.user_agent UserAgent() │
|
|
229
|
+
│ │
|
|
230
|
+
│ ADDRESS │
|
|
231
|
+
│ city Faker::Address.city CityName(EN) │
|
|
232
|
+
│ country Faker::Address.country CountryName(EN) │
|
|
233
|
+
│ country_code Faker::Address.country_code CountryCode(EN) │
|
|
234
|
+
│ street Faker::Address.street_address StreetName(EN) │
|
|
235
|
+
│ postal_code Faker::Address.zip_code ZipCode(EN) │
|
|
236
|
+
│ latitude Faker::Address.latitude Latitude() │
|
|
237
|
+
│ longitude Faker::Address.longitude Longitude() │
|
|
238
|
+
│ state Faker::Address.state StateName(EN) │
|
|
239
|
+
│ │
|
|
240
|
+
│ COMPANY │
|
|
241
|
+
│ company_name Faker::Company.name CompanyName(EN) │
|
|
242
|
+
│ catch_phrase Faker::Company.catch_phrase CatchPhrase(EN) │
|
|
243
|
+
│ industry Faker::Company.industry Industry(EN) │
|
|
244
|
+
│ job_title Faker::Job.title (custom) │
|
|
245
|
+
│ │
|
|
246
|
+
│ TEXT │
|
|
247
|
+
│ word Faker::Lorem.word Word(EN) │
|
|
248
|
+
│ sentence Faker::Lorem.sentence Sentence(EN, 3..10) │
|
|
249
|
+
│ paragraph Faker::Lorem.paragraph Paragraph(EN, 3..5) │
|
|
250
|
+
│ text Faker::Lorem.characters Words(EN, n).join │
|
|
251
|
+
│ │
|
|
252
|
+
│ PHONE │
|
|
253
|
+
│ phone Faker::PhoneNumber.phone PhoneNumber(EN) │
|
|
254
|
+
│ cell_phone Faker::PhoneNumber.cell CellNumber(EN) │
|
|
255
|
+
│ │
|
|
256
|
+
│ DATE/TIME │
|
|
257
|
+
│ date Faker::Date.between Date() │
|
|
258
|
+
│ time Faker::Time.between Time() │
|
|
259
|
+
│ timestamp DateTime.now.to_i DateTime() │
|
|
260
|
+
│ │
|
|
261
|
+
│ IDENTIFIERS │
|
|
262
|
+
│ uuid SecureRandom.uuid UUIDv4() │
|
|
263
|
+
│ uuid_v1 (custom) UUIDv1() │
|
|
264
|
+
│ │
|
|
265
|
+
│ FINANCE │
|
|
266
|
+
│ credit_card Faker::Finance.credit_card CreditCardNumber() │
|
|
267
|
+
│ currency_code Faker::Currency.code CurrencyCode(EN) │
|
|
268
|
+
│ bic Faker::Bank.swift_bic Bic(EN) │
|
|
269
|
+
│ │
|
|
270
|
+
│ PRIMITIVES │
|
|
271
|
+
│ number rand(range) (1..100).fake() │
|
|
272
|
+
│ boolean [true, false].sample Boolean(50) │
|
|
273
|
+
│ enum values.sample rng.sample(&values) │
|
|
274
|
+
│ │
|
|
275
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Locale Support Comparison
|
|
279
|
+
|
|
280
|
+
| Locale | Ruby Faker | fake-rs | Notes |
|
|
281
|
+
|--------|------------|---------|-------|
|
|
282
|
+
| English (EN) | ✅ | ✅ | Full support |
|
|
283
|
+
| French (FR) | ✅ | ✅ | Full support |
|
|
284
|
+
| German (DE) | ✅ | ✅ | Full support |
|
|
285
|
+
| Spanish (ES) | ✅ | ⚠️ | Limited in fake-rs |
|
|
286
|
+
| Japanese (JA) | ✅ | ✅ | Full support |
|
|
287
|
+
| Chinese Simplified (ZH_CN) | ✅ | ✅ | Full support |
|
|
288
|
+
| Chinese Traditional (ZH_TW) | ✅ | ✅ | Full support |
|
|
289
|
+
| Portuguese (PT_BR) | ✅ | ✅ | Full support |
|
|
290
|
+
| Italian (IT) | ✅ | ✅ | Full support |
|
|
291
|
+
| Dutch (NL) | ✅ | ✅ | Full support |
|
|
292
|
+
| Arabic (AR) | ✅ | ✅ | Full support |
|
|
293
|
+
| Russian (RU) | ✅ | ❌ | Not in fake-rs |
|
|
294
|
+
| Korean (KO) | ✅ | ❌ | Not in fake-rs |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Implementation Guide
|
|
299
|
+
|
|
300
|
+
### Project Structure
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
ext/fake_data_dsl_native/
|
|
304
|
+
├── Cargo.toml # Rust dependencies
|
|
305
|
+
├── src/
|
|
306
|
+
│ ├── lib.rs # Main entry point
|
|
307
|
+
│ ├── schema.rs # Schema parsing from Ruby
|
|
308
|
+
│ ├── generator.rs # Generation engine
|
|
309
|
+
│ ├── types.rs # Type mapping to fake-rs
|
|
310
|
+
│ ├── parallel.rs # Rayon parallel processing
|
|
311
|
+
│ └── output.rs # JSON/file output
|
|
312
|
+
├── extconf.rb # Ruby extension config
|
|
313
|
+
└── Rakefile # Build tasks
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Cargo.toml
|
|
317
|
+
|
|
318
|
+
```toml
|
|
319
|
+
[package]
|
|
320
|
+
name = "fake_data_dsl_native"
|
|
321
|
+
version = "0.1.0"
|
|
322
|
+
edition = "2021"
|
|
323
|
+
|
|
324
|
+
[lib]
|
|
325
|
+
crate-type = ["cdylib"]
|
|
326
|
+
|
|
327
|
+
[dependencies]
|
|
328
|
+
# Ruby bindings
|
|
329
|
+
magnus = "0.6"
|
|
330
|
+
|
|
331
|
+
# Fake data generation
|
|
332
|
+
fake = { version = "4", features = [
|
|
333
|
+
"derive",
|
|
334
|
+
"chrono",
|
|
335
|
+
"uuid",
|
|
336
|
+
"random_color",
|
|
337
|
+
"semver",
|
|
338
|
+
"http"
|
|
339
|
+
]}
|
|
340
|
+
|
|
341
|
+
# Random number generation
|
|
342
|
+
rand = "0.8"
|
|
343
|
+
rand_chacha = "0.3"
|
|
344
|
+
|
|
345
|
+
# Parallel processing
|
|
346
|
+
rayon = "1.8"
|
|
347
|
+
|
|
348
|
+
# JSON serialization
|
|
349
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
350
|
+
serde_json = "1.0"
|
|
351
|
+
|
|
352
|
+
# Date/time
|
|
353
|
+
chrono = "0.4"
|
|
354
|
+
|
|
355
|
+
# UUID
|
|
356
|
+
uuid = { version = "1.0", features = ["v4", "v1"] }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### src/lib.rs - Main Entry Point
|
|
360
|
+
|
|
361
|
+
```rust
|
|
362
|
+
use magnus::{define_module, function, prelude::*, Error, RArray, RHash, Ruby};
|
|
363
|
+
|
|
364
|
+
mod schema;
|
|
365
|
+
mod generator;
|
|
366
|
+
mod types;
|
|
367
|
+
mod parallel;
|
|
368
|
+
mod output;
|
|
369
|
+
|
|
370
|
+
use generator::NativeGenerator;
|
|
371
|
+
|
|
372
|
+
/// Generate records and return as Ruby array
|
|
373
|
+
fn generate(
|
|
374
|
+
ruby: &Ruby,
|
|
375
|
+
schema_spec: RArray,
|
|
376
|
+
count: usize,
|
|
377
|
+
seed: Option<u64>,
|
|
378
|
+
locale: Option<String>,
|
|
379
|
+
) -> Result<RArray, Error> {
|
|
380
|
+
let generator = NativeGenerator::new(schema_spec, seed, locale)?;
|
|
381
|
+
generator.generate_to_ruby(ruby, count)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Generate records directly to file (most efficient)
|
|
385
|
+
fn generate_to_file(
|
|
386
|
+
schema_spec: RArray,
|
|
387
|
+
count: usize,
|
|
388
|
+
path: String,
|
|
389
|
+
seed: Option<u64>,
|
|
390
|
+
locale: Option<String>,
|
|
391
|
+
format: Option<String>,
|
|
392
|
+
workers: Option<usize>,
|
|
393
|
+
) -> Result<usize, Error> {
|
|
394
|
+
let generator = NativeGenerator::new(schema_spec, seed, locale)?;
|
|
395
|
+
generator.generate_to_file(count, &path, format.as_deref(), workers)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// Generate records as streaming iterator
|
|
399
|
+
fn generate_stream(
|
|
400
|
+
schema_spec: RArray,
|
|
401
|
+
count: usize,
|
|
402
|
+
seed: Option<u64>,
|
|
403
|
+
locale: Option<String>,
|
|
404
|
+
) -> Result<RArray, Error> {
|
|
405
|
+
// Returns a Ruby Enumerator that yields records one at a time
|
|
406
|
+
todo!("Implement streaming")
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// Get available types
|
|
410
|
+
fn available_types() -> Vec<String> {
|
|
411
|
+
types::AVAILABLE_TYPES.iter().map(|s| s.to_string()).collect()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/// Get available locales
|
|
415
|
+
fn available_locales() -> Vec<String> {
|
|
416
|
+
vec![
|
|
417
|
+
"en".to_string(),
|
|
418
|
+
"fr_fr".to_string(),
|
|
419
|
+
"de_de".to_string(),
|
|
420
|
+
"zh_cn".to_string(),
|
|
421
|
+
"zh_tw".to_string(),
|
|
422
|
+
"ja_jp".to_string(),
|
|
423
|
+
"pt_br".to_string(),
|
|
424
|
+
"it_it".to_string(),
|
|
425
|
+
"nl_nl".to_string(),
|
|
426
|
+
"ar_sa".to_string(),
|
|
427
|
+
]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// Get version
|
|
431
|
+
fn version() -> String {
|
|
432
|
+
env!("CARGO_PKG_VERSION").to_string()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#[magnus::init]
|
|
436
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
437
|
+
let module = ruby.define_module("FakeDataDSL")?;
|
|
438
|
+
let native = module.define_module("Native")?;
|
|
439
|
+
|
|
440
|
+
native.define_singleton_method("generate", function!(generate, 4))?;
|
|
441
|
+
native.define_singleton_method("generate_to_file", function!(generate_to_file, 7))?;
|
|
442
|
+
native.define_singleton_method("generate_stream", function!(generate_stream, 4))?;
|
|
443
|
+
native.define_singleton_method("available_types", function!(available_types, 0))?;
|
|
444
|
+
native.define_singleton_method("available_locales", function!(available_locales, 0))?;
|
|
445
|
+
native.define_singleton_method("version", function!(version, 0))?;
|
|
446
|
+
|
|
447
|
+
Ok(())
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### src/types.rs - Type Mapping
|
|
452
|
+
|
|
453
|
+
```rust
|
|
454
|
+
use fake::faker::name::raw::*;
|
|
455
|
+
use fake::faker::internet::raw::*;
|
|
456
|
+
use fake::faker::address::raw::*;
|
|
457
|
+
use fake::faker::company::raw::*;
|
|
458
|
+
use fake::faker::lorem::raw::*;
|
|
459
|
+
use fake::faker::phone_number::raw::*;
|
|
460
|
+
use fake::faker::chrono::raw::*;
|
|
461
|
+
use fake::faker::boolean::raw::*;
|
|
462
|
+
use fake::locales::*;
|
|
463
|
+
use fake::{Fake, Faker};
|
|
464
|
+
use rand::Rng;
|
|
465
|
+
use uuid::Uuid;
|
|
466
|
+
|
|
467
|
+
pub static AVAILABLE_TYPES: &[&str] = &[
|
|
468
|
+
// Personal
|
|
469
|
+
"name", "first_name", "last_name", "full_name", "title",
|
|
470
|
+
// Internet
|
|
471
|
+
"email", "username", "password", "url", "ip", "ipv4", "ipv6",
|
|
472
|
+
"mac_address", "user_agent", "domain",
|
|
473
|
+
// Address
|
|
474
|
+
"city", "country", "country_code", "street", "street_address",
|
|
475
|
+
"postal_code", "zip_code", "latitude", "longitude", "state",
|
|
476
|
+
// Company
|
|
477
|
+
"company_name", "company", "catch_phrase", "buzzword", "industry",
|
|
478
|
+
// Text
|
|
479
|
+
"word", "words", "sentence", "sentences", "paragraph", "paragraphs", "text",
|
|
480
|
+
// Phone
|
|
481
|
+
"phone", "phone_number", "cell_phone",
|
|
482
|
+
// Date/Time
|
|
483
|
+
"date", "time", "datetime", "timestamp",
|
|
484
|
+
// Identifiers
|
|
485
|
+
"uuid", "uuid_v4", "uuid_v1",
|
|
486
|
+
// Finance
|
|
487
|
+
"credit_card", "currency_code", "bic",
|
|
488
|
+
// Primitives
|
|
489
|
+
"number", "integer", "float", "boolean", "bool",
|
|
490
|
+
// Color
|
|
491
|
+
"hex_color", "rgb_color",
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
/// Locale enum for fake-rs
|
|
495
|
+
#[derive(Clone, Copy)]
|
|
496
|
+
pub enum Locale {
|
|
497
|
+
EN,
|
|
498
|
+
FR,
|
|
499
|
+
DE,
|
|
500
|
+
ZH_CN,
|
|
501
|
+
ZH_TW,
|
|
502
|
+
JA,
|
|
503
|
+
PT_BR,
|
|
504
|
+
IT,
|
|
505
|
+
NL,
|
|
506
|
+
AR,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
impl Locale {
|
|
510
|
+
pub fn from_string(s: &str) -> Self {
|
|
511
|
+
match s.to_lowercase().as_str() {
|
|
512
|
+
"fr" | "fr_fr" => Locale::FR,
|
|
513
|
+
"de" | "de_de" => Locale::DE,
|
|
514
|
+
"zh_cn" => Locale::ZH_CN,
|
|
515
|
+
"zh_tw" => Locale::ZH_TW,
|
|
516
|
+
"ja" | "ja_jp" => Locale::JA,
|
|
517
|
+
"pt_br" => Locale::PT_BR,
|
|
518
|
+
"it" | "it_it" => Locale::IT,
|
|
519
|
+
"nl" | "nl_nl" => Locale::NL,
|
|
520
|
+
"ar" | "ar_sa" => Locale::AR,
|
|
521
|
+
_ => Locale::EN,
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// Generate a value for a given type
|
|
527
|
+
pub fn generate_value<R: Rng>(
|
|
528
|
+
rng: &mut R,
|
|
529
|
+
type_name: &str,
|
|
530
|
+
args: &serde_json::Value,
|
|
531
|
+
locale: Locale,
|
|
532
|
+
index: usize,
|
|
533
|
+
) -> serde_json::Value {
|
|
534
|
+
match type_name {
|
|
535
|
+
// Personal names
|
|
536
|
+
"name" | "full_name" => generate_name(rng, locale),
|
|
537
|
+
"first_name" => generate_first_name(rng, locale),
|
|
538
|
+
"last_name" => generate_last_name(rng, locale),
|
|
539
|
+
"title" => generate_title(rng, locale),
|
|
540
|
+
|
|
541
|
+
// Internet
|
|
542
|
+
"email" => generate_email(rng, locale, index),
|
|
543
|
+
"username" => generate_username(rng, locale),
|
|
544
|
+
"password" => generate_password(rng, args),
|
|
545
|
+
"ip" | "ipv4" => generate_ipv4(rng),
|
|
546
|
+
"ipv6" => generate_ipv6(rng),
|
|
547
|
+
"mac_address" => generate_mac_address(rng),
|
|
548
|
+
"user_agent" => generate_user_agent(rng),
|
|
549
|
+
"domain" => generate_domain(rng, locale),
|
|
550
|
+
|
|
551
|
+
// Address
|
|
552
|
+
"city" => generate_city(rng, locale),
|
|
553
|
+
"country" => generate_country(rng, locale),
|
|
554
|
+
"country_code" => generate_country_code(rng, locale),
|
|
555
|
+
"street" | "street_address" => generate_street(rng, locale),
|
|
556
|
+
"postal_code" | "zip_code" => generate_zip_code(rng, locale),
|
|
557
|
+
"latitude" => generate_latitude(rng),
|
|
558
|
+
"longitude" => generate_longitude(rng),
|
|
559
|
+
"state" => generate_state(rng, locale),
|
|
560
|
+
|
|
561
|
+
// Company
|
|
562
|
+
"company" | "company_name" => generate_company(rng, locale),
|
|
563
|
+
"catch_phrase" => generate_catch_phrase(rng, locale),
|
|
564
|
+
"buzzword" => generate_buzzword(rng, locale),
|
|
565
|
+
"industry" => generate_industry(rng, locale),
|
|
566
|
+
|
|
567
|
+
// Text
|
|
568
|
+
"word" => generate_word(rng, locale),
|
|
569
|
+
"words" => generate_words(rng, locale, args),
|
|
570
|
+
"sentence" => generate_sentence(rng, locale, args),
|
|
571
|
+
"paragraph" => generate_paragraph(rng, locale, args),
|
|
572
|
+
"text" => generate_text(rng, args),
|
|
573
|
+
|
|
574
|
+
// Phone
|
|
575
|
+
"phone" | "phone_number" => generate_phone(rng, locale),
|
|
576
|
+
"cell_phone" => generate_cell_phone(rng, locale),
|
|
577
|
+
|
|
578
|
+
// Date/Time
|
|
579
|
+
"date" => generate_date(rng),
|
|
580
|
+
"time" => generate_time(rng),
|
|
581
|
+
"datetime" | "timestamp" => generate_datetime(rng),
|
|
582
|
+
|
|
583
|
+
// Identifiers
|
|
584
|
+
"uuid" | "uuid_v4" => generate_uuid_v4(),
|
|
585
|
+
"uuid_v1" => generate_uuid_v1(index),
|
|
586
|
+
|
|
587
|
+
// Finance
|
|
588
|
+
"credit_card" => generate_credit_card(rng),
|
|
589
|
+
"currency_code" => generate_currency_code(rng, locale),
|
|
590
|
+
"bic" => generate_bic(rng, locale),
|
|
591
|
+
|
|
592
|
+
// Primitives
|
|
593
|
+
"number" | "integer" => generate_number(rng, args),
|
|
594
|
+
"float" => generate_float(rng, args),
|
|
595
|
+
"boolean" | "bool" => generate_boolean(rng, args),
|
|
596
|
+
|
|
597
|
+
// Color
|
|
598
|
+
"hex_color" => generate_hex_color(rng),
|
|
599
|
+
"rgb_color" => generate_rgb_color(rng),
|
|
600
|
+
|
|
601
|
+
// Enum
|
|
602
|
+
"enum" => generate_enum(rng, args),
|
|
603
|
+
|
|
604
|
+
// Const
|
|
605
|
+
"const" => generate_const(args),
|
|
606
|
+
|
|
607
|
+
// Unknown - return null
|
|
608
|
+
_ => serde_json::Value::Null,
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// Generator implementations using fake-rs
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
fn generate_name<R: Rng>(rng: &mut R, locale: Locale) -> serde_json::Value {
|
|
617
|
+
let name: String = match locale {
|
|
618
|
+
Locale::EN => Name(EN).fake_with_rng(rng),
|
|
619
|
+
Locale::FR => Name(FR_FR).fake_with_rng(rng),
|
|
620
|
+
Locale::DE => Name(DE_DE).fake_with_rng(rng),
|
|
621
|
+
Locale::ZH_CN => Name(ZH_CN).fake_with_rng(rng),
|
|
622
|
+
Locale::ZH_TW => Name(ZH_TW).fake_with_rng(rng),
|
|
623
|
+
Locale::JA => Name(JA_JP).fake_with_rng(rng),
|
|
624
|
+
Locale::PT_BR => Name(PT_BR).fake_with_rng(rng),
|
|
625
|
+
Locale::IT => Name(IT_IT).fake_with_rng(rng),
|
|
626
|
+
Locale::NL => Name(NL_NL).fake_with_rng(rng),
|
|
627
|
+
Locale::AR => Name(AR_SA).fake_with_rng(rng),
|
|
628
|
+
};
|
|
629
|
+
serde_json::Value::String(name)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
fn generate_first_name<R: Rng>(rng: &mut R, locale: Locale) -> serde_json::Value {
|
|
633
|
+
let name: String = match locale {
|
|
634
|
+
Locale::EN => FirstName(EN).fake_with_rng(rng),
|
|
635
|
+
Locale::FR => FirstName(FR_FR).fake_with_rng(rng),
|
|
636
|
+
Locale::DE => FirstName(DE_DE).fake_with_rng(rng),
|
|
637
|
+
Locale::ZH_CN => FirstName(ZH_CN).fake_with_rng(rng),
|
|
638
|
+
Locale::ZH_TW => FirstName(ZH_TW).fake_with_rng(rng),
|
|
639
|
+
Locale::JA => FirstName(JA_JP).fake_with_rng(rng),
|
|
640
|
+
Locale::PT_BR => FirstName(PT_BR).fake_with_rng(rng),
|
|
641
|
+
Locale::IT => FirstName(IT_IT).fake_with_rng(rng),
|
|
642
|
+
Locale::NL => FirstName(NL_NL).fake_with_rng(rng),
|
|
643
|
+
Locale::AR => FirstName(AR_SA).fake_with_rng(rng),
|
|
644
|
+
};
|
|
645
|
+
serde_json::Value::String(name)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
fn generate_last_name<R: Rng>(rng: &mut R, locale: Locale) -> serde_json::Value {
|
|
649
|
+
let name: String = match locale {
|
|
650
|
+
Locale::EN => LastName(EN).fake_with_rng(rng),
|
|
651
|
+
Locale::FR => LastName(FR_FR).fake_with_rng(rng),
|
|
652
|
+
Locale::DE => LastName(DE_DE).fake_with_rng(rng),
|
|
653
|
+
Locale::ZH_CN => LastName(ZH_CN).fake_with_rng(rng),
|
|
654
|
+
Locale::ZH_TW => LastName(ZH_TW).fake_with_rng(rng),
|
|
655
|
+
Locale::JA => LastName(JA_JP).fake_with_rng(rng),
|
|
656
|
+
Locale::PT_BR => LastName(PT_BR).fake_with_rng(rng),
|
|
657
|
+
Locale::IT => LastName(IT_IT).fake_with_rng(rng),
|
|
658
|
+
Locale::NL => LastName(NL_NL).fake_with_rng(rng),
|
|
659
|
+
Locale::AR => LastName(AR_SA).fake_with_rng(rng),
|
|
660
|
+
};
|
|
661
|
+
serde_json::Value::String(name)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
fn generate_email<R: Rng>(rng: &mut R, locale: Locale, index: usize) -> serde_json::Value {
|
|
665
|
+
// Generate base email and make unique with index
|
|
666
|
+
let base: String = FreeEmail(EN).fake_with_rng(rng);
|
|
667
|
+
let parts: Vec<&str> = base.splitn(2, '@').collect();
|
|
668
|
+
if parts.len() == 2 {
|
|
669
|
+
let unique_email = format!("{}{}@{}", parts[0], index, parts[1]);
|
|
670
|
+
serde_json::Value::String(unique_email)
|
|
671
|
+
} else {
|
|
672
|
+
serde_json::Value::String(base)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn generate_username<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
677
|
+
let username: String = Username(EN).fake_with_rng(rng);
|
|
678
|
+
serde_json::Value::String(username)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
fn generate_password<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
682
|
+
let min = args.get("min").and_then(|v| v.as_u64()).unwrap_or(8) as usize;
|
|
683
|
+
let max = args.get("max").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
|
684
|
+
let password: String = Password(EN, min..max).fake_with_rng(rng);
|
|
685
|
+
serde_json::Value::String(password)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn generate_ipv4<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
689
|
+
let ip: String = IPv4(EN).fake_with_rng(rng);
|
|
690
|
+
serde_json::Value::String(ip)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
fn generate_ipv6<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
694
|
+
let ip: String = IPv6(EN).fake_with_rng(rng);
|
|
695
|
+
serde_json::Value::String(ip)
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
fn generate_mac_address<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
699
|
+
let mac: String = MACAddress(EN).fake_with_rng(rng);
|
|
700
|
+
serde_json::Value::String(mac)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fn generate_user_agent<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
704
|
+
let ua: String = UserAgent(EN).fake_with_rng(rng);
|
|
705
|
+
serde_json::Value::String(ua)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
fn generate_domain<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
709
|
+
let domain: String = DomainSuffix(EN).fake_with_rng(rng);
|
|
710
|
+
serde_json::Value::String(domain)
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
fn generate_city<R: Rng>(rng: &mut R, locale: Locale) -> serde_json::Value {
|
|
714
|
+
let city: String = match locale {
|
|
715
|
+
Locale::EN => CityName(EN).fake_with_rng(rng),
|
|
716
|
+
Locale::FR => CityName(FR_FR).fake_with_rng(rng),
|
|
717
|
+
Locale::DE => CityName(DE_DE).fake_with_rng(rng),
|
|
718
|
+
_ => CityName(EN).fake_with_rng(rng),
|
|
719
|
+
};
|
|
720
|
+
serde_json::Value::String(city)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
fn generate_country<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
724
|
+
let country: String = CountryName(EN).fake_with_rng(rng);
|
|
725
|
+
serde_json::Value::String(country)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
fn generate_country_code<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
729
|
+
let code: String = CountryCode(EN).fake_with_rng(rng);
|
|
730
|
+
serde_json::Value::String(code)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
fn generate_street<R: Rng>(rng: &mut R, locale: Locale) -> serde_json::Value {
|
|
734
|
+
let street: String = match locale {
|
|
735
|
+
Locale::EN => StreetName(EN).fake_with_rng(rng),
|
|
736
|
+
Locale::FR => StreetName(FR_FR).fake_with_rng(rng),
|
|
737
|
+
_ => StreetName(EN).fake_with_rng(rng),
|
|
738
|
+
};
|
|
739
|
+
serde_json::Value::String(street)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
fn generate_zip_code<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
743
|
+
let zip: String = ZipCode(EN).fake_with_rng(rng);
|
|
744
|
+
serde_json::Value::String(zip)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
fn generate_latitude<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
748
|
+
let lat: f64 = Latitude(EN).fake_with_rng(rng);
|
|
749
|
+
serde_json::Value::Number(serde_json::Number::from_f64(lat).unwrap())
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
fn generate_longitude<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
753
|
+
let lng: f64 = Longitude(EN).fake_with_rng(rng);
|
|
754
|
+
serde_json::Value::Number(serde_json::Number::from_f64(lng).unwrap())
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
fn generate_state<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
758
|
+
let state: String = StateName(EN).fake_with_rng(rng);
|
|
759
|
+
serde_json::Value::String(state)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn generate_company<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
763
|
+
let company: String = CompanyName(EN).fake_with_rng(rng);
|
|
764
|
+
serde_json::Value::String(company)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
fn generate_catch_phrase<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
768
|
+
let phrase: String = CatchPhrase(EN).fake_with_rng(rng);
|
|
769
|
+
serde_json::Value::String(phrase)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
fn generate_buzzword<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
773
|
+
let buzzword: String = Buzzword(EN).fake_with_rng(rng);
|
|
774
|
+
serde_json::Value::String(buzzword)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fn generate_industry<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
778
|
+
let industry: String = Industry(EN).fake_with_rng(rng);
|
|
779
|
+
serde_json::Value::String(industry)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
fn generate_word<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
783
|
+
let word: String = Word(EN).fake_with_rng(rng);
|
|
784
|
+
serde_json::Value::String(word)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fn generate_words<R: Rng>(rng: &mut R, _locale: Locale, args: &serde_json::Value) -> serde_json::Value {
|
|
788
|
+
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
|
789
|
+
let words: Vec<String> = Words(EN, count..count+1).fake_with_rng(rng);
|
|
790
|
+
serde_json::Value::String(words.join(" "))
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
fn generate_sentence<R: Rng>(rng: &mut R, _locale: Locale, args: &serde_json::Value) -> serde_json::Value {
|
|
794
|
+
let count = args.get("words").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
|
795
|
+
let sentence: String = Sentence(EN, count..count+5).fake_with_rng(rng);
|
|
796
|
+
serde_json::Value::String(sentence)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
fn generate_paragraph<R: Rng>(rng: &mut R, _locale: Locale, args: &serde_json::Value) -> serde_json::Value {
|
|
800
|
+
let count = args.get("sentences").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
|
801
|
+
let para: String = Paragraph(EN, count..count+3).fake_with_rng(rng);
|
|
802
|
+
serde_json::Value::String(para)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
fn generate_text<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
806
|
+
let min = args.get("min").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
|
807
|
+
let max = args.get("max").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
|
808
|
+
let len = rng.gen_range(min..=max);
|
|
809
|
+
let text: String = (0..len).map(|_| rng.gen_range(b'a'..=b'z') as char).collect();
|
|
810
|
+
serde_json::Value::String(text)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
fn generate_phone<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
814
|
+
let phone: String = PhoneNumber(EN).fake_with_rng(rng);
|
|
815
|
+
serde_json::Value::String(phone)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
fn generate_cell_phone<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
819
|
+
let phone: String = CellNumber(EN).fake_with_rng(rng);
|
|
820
|
+
serde_json::Value::String(phone)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
fn generate_date<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
824
|
+
use chrono::{Duration, Utc};
|
|
825
|
+
let days_ago = rng.gen_range(0..365);
|
|
826
|
+
let date = Utc::now() - Duration::days(days_ago);
|
|
827
|
+
serde_json::Value::String(date.format("%Y-%m-%d").to_string())
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
fn generate_time<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
831
|
+
let hour = rng.gen_range(0..24);
|
|
832
|
+
let minute = rng.gen_range(0..60);
|
|
833
|
+
let second = rng.gen_range(0..60);
|
|
834
|
+
serde_json::Value::String(format!("{:02}:{:02}:{:02}", hour, minute, second))
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
fn generate_datetime<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
838
|
+
use chrono::{Duration, Utc};
|
|
839
|
+
let days_ago = rng.gen_range(0..365);
|
|
840
|
+
let dt = Utc::now() - Duration::days(days_ago);
|
|
841
|
+
serde_json::Value::String(dt.to_rfc3339())
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
fn generate_uuid_v4() -> serde_json::Value {
|
|
845
|
+
serde_json::Value::String(Uuid::new_v4().to_string())
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
fn generate_uuid_v1(index: usize) -> serde_json::Value {
|
|
849
|
+
// Simple sequential UUID using index
|
|
850
|
+
serde_json::Value::String(format!(
|
|
851
|
+
"{:08x}-{:04x}-1000-8000-{:012x}",
|
|
852
|
+
(index >> 32) as u32,
|
|
853
|
+
(index >> 16) as u16,
|
|
854
|
+
index as u64
|
|
855
|
+
))
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
fn generate_credit_card<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
859
|
+
// Generate a fake credit card number
|
|
860
|
+
let prefix = ["4", "5", "37", "6011"][rng.gen_range(0..4)];
|
|
861
|
+
let len = if prefix == "37" { 15 } else { 16 };
|
|
862
|
+
let mut number = prefix.to_string();
|
|
863
|
+
for _ in prefix.len()..len {
|
|
864
|
+
number.push_str(&rng.gen_range(0..10).to_string());
|
|
865
|
+
}
|
|
866
|
+
serde_json::Value::String(number)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
fn generate_currency_code<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
870
|
+
let codes = ["USD", "EUR", "GBP", "JPY", "CNY", "CAD", "AUD", "CHF"];
|
|
871
|
+
serde_json::Value::String(codes[rng.gen_range(0..codes.len())].to_string())
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
fn generate_bic<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
875
|
+
let bic: String = Bic(EN).fake_with_rng(rng);
|
|
876
|
+
serde_json::Value::String(bic)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
fn generate_number<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
880
|
+
let min = args.get("min").and_then(|v| v.as_i64()).unwrap_or(0);
|
|
881
|
+
let max = args.get("max").and_then(|v| v.as_i64()).unwrap_or(100);
|
|
882
|
+
serde_json::Value::Number(rng.gen_range(min..=max).into())
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
fn generate_float<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
886
|
+
let min = args.get("min").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
887
|
+
let max = args.get("max").and_then(|v| v.as_f64()).unwrap_or(100.0);
|
|
888
|
+
let val = rng.gen_range(min..=max);
|
|
889
|
+
serde_json::Value::Number(serde_json::Number::from_f64(val).unwrap())
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
fn generate_boolean<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
893
|
+
let prob = args.get("probability")
|
|
894
|
+
.or_else(|| args.get("true_probability"))
|
|
895
|
+
.and_then(|v| v.as_f64())
|
|
896
|
+
.unwrap_or(0.5);
|
|
897
|
+
serde_json::Value::Bool(rng.gen_bool(prob))
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
fn generate_hex_color<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
901
|
+
let color: String = HexColor(EN).fake_with_rng(rng);
|
|
902
|
+
serde_json::Value::String(color)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fn generate_rgb_color<R: Rng>(rng: &mut R) -> serde_json::Value {
|
|
906
|
+
let r = rng.gen_range(0..256);
|
|
907
|
+
let g = rng.gen_range(0..256);
|
|
908
|
+
let b = rng.gen_range(0..256);
|
|
909
|
+
serde_json::Value::String(format!("rgb({}, {}, {})", r, g, b))
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
fn generate_enum<R: Rng>(rng: &mut R, args: &serde_json::Value) -> serde_json::Value {
|
|
913
|
+
if let Some(values) = args.get("values").and_then(|v| v.as_array()) {
|
|
914
|
+
if !values.is_empty() {
|
|
915
|
+
return values[rng.gen_range(0..values.len())].clone();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
serde_json::Value::Null
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fn generate_const(args: &serde_json::Value) -> serde_json::Value {
|
|
922
|
+
args.get("value").cloned().unwrap_or(serde_json::Value::Null)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
fn generate_title<R: Rng>(rng: &mut R, _locale: Locale) -> serde_json::Value {
|
|
926
|
+
let title: String = Title(EN).fake_with_rng(rng);
|
|
927
|
+
serde_json::Value::String(title)
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
### src/parallel.rs - Parallel Generation
|
|
932
|
+
|
|
933
|
+
```rust
|
|
934
|
+
use rayon::prelude::*;
|
|
935
|
+
use rand::SeedableRng;
|
|
936
|
+
use rand_chacha::ChaCha8Rng;
|
|
937
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
938
|
+
|
|
939
|
+
use crate::types::{generate_value, Locale};
|
|
940
|
+
|
|
941
|
+
pub struct ParallelGenerator {
|
|
942
|
+
field_specs: Vec<FieldSpec>,
|
|
943
|
+
locale: Locale,
|
|
944
|
+
base_seed: u64,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
#[derive(Clone)]
|
|
948
|
+
pub struct FieldSpec {
|
|
949
|
+
pub name: String,
|
|
950
|
+
pub type_name: String,
|
|
951
|
+
pub args: serde_json::Value,
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
impl ParallelGenerator {
|
|
955
|
+
pub fn new(field_specs: Vec<FieldSpec>, locale: Locale, seed: u64) -> Self {
|
|
956
|
+
Self {
|
|
957
|
+
field_specs,
|
|
958
|
+
locale,
|
|
959
|
+
base_seed: seed,
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/// Generate records in parallel using all available cores
|
|
964
|
+
pub fn generate_parallel(&self, count: usize, workers: Option<usize>) -> Vec<serde_json::Map<String, serde_json::Value>> {
|
|
965
|
+
// Configure thread pool
|
|
966
|
+
if let Some(w) = workers {
|
|
967
|
+
rayon::ThreadPoolBuilder::new()
|
|
968
|
+
.num_threads(w)
|
|
969
|
+
.build_global()
|
|
970
|
+
.ok();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let counter = AtomicUsize::new(0);
|
|
974
|
+
|
|
975
|
+
(0..count)
|
|
976
|
+
.into_par_iter()
|
|
977
|
+
.map(|i| {
|
|
978
|
+
// Each thread gets its own RNG seeded from base_seed + index
|
|
979
|
+
let mut rng = ChaCha8Rng::seed_from_u64(self.base_seed.wrapping_add(i as u64));
|
|
980
|
+
let index = counter.fetch_add(1, Ordering::Relaxed);
|
|
981
|
+
|
|
982
|
+
let mut record = serde_json::Map::new();
|
|
983
|
+
|
|
984
|
+
for spec in &self.field_specs {
|
|
985
|
+
let value = generate_value(
|
|
986
|
+
&mut rng,
|
|
987
|
+
&spec.type_name,
|
|
988
|
+
&spec.args,
|
|
989
|
+
self.locale,
|
|
990
|
+
index,
|
|
991
|
+
);
|
|
992
|
+
record.insert(spec.name.clone(), value);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
record
|
|
996
|
+
})
|
|
997
|
+
.collect()
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/// Generate directly to file in parallel (most efficient)
|
|
1001
|
+
pub fn generate_to_file_parallel(
|
|
1002
|
+
&self,
|
|
1003
|
+
count: usize,
|
|
1004
|
+
path: &str,
|
|
1005
|
+
format: &str,
|
|
1006
|
+
workers: Option<usize>,
|
|
1007
|
+
) -> std::io::Result<usize> {
|
|
1008
|
+
use std::fs::File;
|
|
1009
|
+
use std::io::{BufWriter, Write};
|
|
1010
|
+
|
|
1011
|
+
// Configure thread pool
|
|
1012
|
+
let num_workers = workers.unwrap_or_else(|| rayon::current_num_threads());
|
|
1013
|
+
|
|
1014
|
+
// Split work across workers
|
|
1015
|
+
let chunk_size = (count + num_workers - 1) / num_workers;
|
|
1016
|
+
|
|
1017
|
+
// Generate chunks in parallel
|
|
1018
|
+
let chunks: Vec<Vec<String>> = (0..num_workers)
|
|
1019
|
+
.into_par_iter()
|
|
1020
|
+
.map(|worker_id| {
|
|
1021
|
+
let start = worker_id * chunk_size;
|
|
1022
|
+
let end = std::cmp::min(start + chunk_size, count);
|
|
1023
|
+
|
|
1024
|
+
let mut rng = ChaCha8Rng::seed_from_u64(
|
|
1025
|
+
self.base_seed.wrapping_add(start as u64)
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
let mut lines = Vec::with_capacity(end - start);
|
|
1029
|
+
|
|
1030
|
+
for i in start..end {
|
|
1031
|
+
let mut record = serde_json::Map::new();
|
|
1032
|
+
|
|
1033
|
+
for spec in &self.field_specs {
|
|
1034
|
+
let value = generate_value(
|
|
1035
|
+
&mut rng,
|
|
1036
|
+
&spec.type_name,
|
|
1037
|
+
&spec.args,
|
|
1038
|
+
self.locale,
|
|
1039
|
+
i,
|
|
1040
|
+
);
|
|
1041
|
+
record.insert(spec.name.clone(), value);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
match format {
|
|
1045
|
+
"jsonl" | "json" => {
|
|
1046
|
+
lines.push(serde_json::to_string(&record).unwrap());
|
|
1047
|
+
}
|
|
1048
|
+
"csv" => {
|
|
1049
|
+
let values: Vec<String> = self.field_specs.iter()
|
|
1050
|
+
.map(|spec| {
|
|
1051
|
+
let v = record.get(&spec.name).unwrap();
|
|
1052
|
+
match v {
|
|
1053
|
+
serde_json::Value::String(s) => {
|
|
1054
|
+
if s.contains(',') || s.contains('"') {
|
|
1055
|
+
format!("\"{}\"", s.replace('"', "\"\""))
|
|
1056
|
+
} else {
|
|
1057
|
+
s.clone()
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
_ => v.to_string()
|
|
1061
|
+
}
|
|
1062
|
+
})
|
|
1063
|
+
.collect();
|
|
1064
|
+
lines.push(values.join(","));
|
|
1065
|
+
}
|
|
1066
|
+
_ => {
|
|
1067
|
+
lines.push(serde_json::to_string(&record).unwrap());
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
lines
|
|
1073
|
+
})
|
|
1074
|
+
.collect();
|
|
1075
|
+
|
|
1076
|
+
// Write sequentially (I/O is the bottleneck anyway)
|
|
1077
|
+
let file = File::create(path)?;
|
|
1078
|
+
let mut writer = BufWriter::with_capacity(64 * 1024 * 1024, file);
|
|
1079
|
+
|
|
1080
|
+
// Write CSV header if needed
|
|
1081
|
+
if format == "csv" {
|
|
1082
|
+
let headers: Vec<&str> = self.field_specs.iter()
|
|
1083
|
+
.map(|s| s.name.as_str())
|
|
1084
|
+
.collect();
|
|
1085
|
+
writeln!(writer, "{}", headers.join(","))?;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
for chunk in chunks {
|
|
1089
|
+
for line in chunk {
|
|
1090
|
+
writeln!(writer, "{}", line)?;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
writer.flush()?;
|
|
1095
|
+
Ok(count)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## API Design
|
|
1103
|
+
|
|
1104
|
+
### Ruby Interface
|
|
1105
|
+
|
|
1106
|
+
```ruby
|
|
1107
|
+
# lib/fake_data_dsl/native_engine.rb
|
|
1108
|
+
|
|
1109
|
+
module FakeDataDSL
|
|
1110
|
+
# High-performance native engine using Rust + fake-rs
|
|
1111
|
+
class NativeEngine
|
|
1112
|
+
def initialize(schema, locale: 'en')
|
|
1113
|
+
@schema = schema
|
|
1114
|
+
@locale = locale
|
|
1115
|
+
@field_specs = build_field_specs
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# Generate records (returns Ruby array)
|
|
1119
|
+
def generate_many(count, seed: nil)
|
|
1120
|
+
Native.generate(@field_specs, count, seed, @locale)
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
# Generate to file (most efficient)
|
|
1124
|
+
def generate_to_file(count, path, seed: nil, format: 'jsonl', workers: nil)
|
|
1125
|
+
Native.generate_to_file(
|
|
1126
|
+
@field_specs, count, path, seed, @locale, format, workers
|
|
1127
|
+
)
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
# Stream (lazy enumeration)
|
|
1131
|
+
def generate_stream(count, seed: nil)
|
|
1132
|
+
Native.generate_stream(@field_specs, count, seed, @locale)
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# Check if native extension is available
|
|
1136
|
+
def self.available?
|
|
1137
|
+
defined?(Native) && Native.respond_to?(:generate)
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
# Get supported types
|
|
1141
|
+
def self.supported_types
|
|
1142
|
+
Native.available_types
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Get supported locales
|
|
1146
|
+
def self.supported_locales
|
|
1147
|
+
Native.available_locales
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
private
|
|
1151
|
+
|
|
1152
|
+
def build_field_specs
|
|
1153
|
+
@schema.fields.map do |field|
|
|
1154
|
+
[
|
|
1155
|
+
field.name,
|
|
1156
|
+
field.type_name,
|
|
1157
|
+
field.type_args.to_json
|
|
1158
|
+
]
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
end
|
|
1162
|
+
end
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### Usage Examples
|
|
1166
|
+
|
|
1167
|
+
```ruby
|
|
1168
|
+
# Basic usage
|
|
1169
|
+
schema = FakeDataDSL.parse(<<~DSL)
|
|
1170
|
+
User:
|
|
1171
|
+
id: uuid
|
|
1172
|
+
name: name
|
|
1173
|
+
email: email
|
|
1174
|
+
age: number(18..80)
|
|
1175
|
+
active: boolean
|
|
1176
|
+
DSL
|
|
1177
|
+
|
|
1178
|
+
# Check if native engine is available
|
|
1179
|
+
if FakeDataDSL::NativeEngine.available?
|
|
1180
|
+
engine = FakeDataDSL::NativeEngine.new(schema, locale: 'en')
|
|
1181
|
+
|
|
1182
|
+
# Generate 10 million records to file in ~2 seconds
|
|
1183
|
+
engine.generate_to_file(10_000_000, '/tmp/users.jsonl', seed: 42)
|
|
1184
|
+
|
|
1185
|
+
# Generate with specific locale
|
|
1186
|
+
engine = FakeDataDSL::NativeEngine.new(schema, locale: 'ja_jp')
|
|
1187
|
+
engine.generate_to_file(1_000_000, '/tmp/users_jp.jsonl')
|
|
1188
|
+
else
|
|
1189
|
+
# Fall back to Ruby engine
|
|
1190
|
+
engine = FakeDataDSL::Generator::CachedFakerEngine.new(schema)
|
|
1191
|
+
engine.to_file(10_000_000, '/tmp/users.jsonl', seed: 42)
|
|
1192
|
+
end
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
## Building and Installation
|
|
1198
|
+
|
|
1199
|
+
### Prerequisites
|
|
1200
|
+
|
|
1201
|
+
```bash
|
|
1202
|
+
# Install Rust
|
|
1203
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
1204
|
+
|
|
1205
|
+
# Verify installation
|
|
1206
|
+
rustc --version
|
|
1207
|
+
cargo --version
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### Build the Extension
|
|
1211
|
+
|
|
1212
|
+
```bash
|
|
1213
|
+
# Navigate to extension directory
|
|
1214
|
+
cd ext/fake_data_dsl_native
|
|
1215
|
+
|
|
1216
|
+
# Build with cargo
|
|
1217
|
+
cargo build --release
|
|
1218
|
+
|
|
1219
|
+
# Or use rake
|
|
1220
|
+
bundle exec rake native:build
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
### Install the Gem with Native Extension
|
|
1224
|
+
|
|
1225
|
+
```ruby
|
|
1226
|
+
# Gemfile
|
|
1227
|
+
gem 'fake_data_dsl', require: 'fake_data_dsl'
|
|
1228
|
+
|
|
1229
|
+
# The gem will automatically compile the native extension if Rust is available
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Verify Installation
|
|
1233
|
+
|
|
1234
|
+
```ruby
|
|
1235
|
+
require 'fake_data_dsl'
|
|
1236
|
+
|
|
1237
|
+
if FakeDataDSL::NativeEngine.available?
|
|
1238
|
+
puts "Native engine available!"
|
|
1239
|
+
puts "Supported types: #{FakeDataDSL::NativeEngine.supported_types}"
|
|
1240
|
+
puts "Supported locales: #{FakeDataDSL::NativeEngine.supported_locales}"
|
|
1241
|
+
else
|
|
1242
|
+
puts "Native engine not available, using Ruby engine"
|
|
1243
|
+
end
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
---
|
|
1247
|
+
|
|
1248
|
+
## Performance Summary
|
|
1249
|
+
|
|
1250
|
+
| Approach | Records/sec | 10M Time | Faker Quality | Locales |
|
|
1251
|
+
|----------|-------------|----------|---------------|---------|
|
|
1252
|
+
| Ruby + Faker | 20K | 8 min | ✅ Full | ✅ 60+ |
|
|
1253
|
+
| Ruby Cached | 300K | 35 sec | ✅ Full | ✅ 60+ |
|
|
1254
|
+
| **Rust + fake-rs** | **5M** | **2 sec** | ✅ Full | ✅ 12 |
|
|
1255
|
+
| Rust (no faker) | 10M | 1 sec | ❌ None | N/A |
|
|
1256
|
+
|
|
1257
|
+
**Best of both worlds:** Rust + fake-rs gives you full faker functionality with native performance!
|
|
1258
|
+
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
## References
|
|
1262
|
+
|
|
1263
|
+
- [fake-rs GitHub Repository](https://github.com/cksac/fake-rs)
|
|
1264
|
+
- [Magnus - Ruby bindings for Rust](https://github.com/matsadler/magnus)
|
|
1265
|
+
- [Rayon - Parallel processing in Rust](https://github.com/rayon-rs/rayon)
|
|
1266
|
+
|
|
1267
|
+
---
|
|
1268
|
+
|
|
1269
|
+
*Last updated: January 2026*
|
|
1270
|
+
|