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,1376 @@
|
|
|
1
|
+
# Integrating Rust fake-rs into FakeDataDSL Ruby Gem
|
|
2
|
+
|
|
3
|
+
A step-by-step guide to adding a high-performance Rust native extension to FakeDataDSL using [fake-rs](https://github.com/cksac/fake-rs) and [Magnus](https://github.com/matsadler/magnus) (Ruby-Rust bindings).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Prerequisites](#1-prerequisites)
|
|
10
|
+
2. [Project Structure](#2-project-structure)
|
|
11
|
+
3. [Setting Up the Rust Extension](#3-setting-up-the-rust-extension)
|
|
12
|
+
4. [Writing the Rust Code](#4-writing-the-rust-code)
|
|
13
|
+
5. [Building the Extension](#5-building-the-extension)
|
|
14
|
+
6. [Integrating with Ruby](#6-integrating-with-ruby)
|
|
15
|
+
7. [Testing](#7-testing)
|
|
16
|
+
8. [Packaging the Gem](#8-packaging-the-gem)
|
|
17
|
+
9. [Performance Benchmarks](#9-performance-benchmarks)
|
|
18
|
+
10. [Troubleshooting](#10-troubleshooting)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Prerequisites
|
|
23
|
+
|
|
24
|
+
### Install Rust
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# macOS/Linux
|
|
28
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
29
|
+
|
|
30
|
+
# Verify installation
|
|
31
|
+
rustc --version # Should show 1.70+
|
|
32
|
+
cargo --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Install rb-sys (Ruby-Rust bridge)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gem install rb_sys
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### System Dependencies
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# macOS
|
|
45
|
+
xcode-select --install
|
|
46
|
+
|
|
47
|
+
# Ubuntu/Debian
|
|
48
|
+
sudo apt-get install build-essential libclang-dev
|
|
49
|
+
|
|
50
|
+
# Fedora/RHEL
|
|
51
|
+
sudo dnf install clang-devel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. Project Structure
|
|
57
|
+
|
|
58
|
+
Add these files/directories to your gem:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
fake_data_dsl/
|
|
62
|
+
├── ext/
|
|
63
|
+
│ └── fake_data_dsl_native/ # Rust extension
|
|
64
|
+
│ ├── Cargo.toml # Rust dependencies
|
|
65
|
+
│ ├── extconf.rb # Ruby extension config
|
|
66
|
+
│ └── src/
|
|
67
|
+
│ ├── lib.rs # Main entry point
|
|
68
|
+
│ ├── types.rs # Type generators
|
|
69
|
+
│ ├── generator.rs # Generation engine
|
|
70
|
+
│ └── schema.rs # Schema parsing
|
|
71
|
+
├── lib/
|
|
72
|
+
│ └── fake_data_dsl/
|
|
73
|
+
│ ├── native_engine.rb # Ruby wrapper
|
|
74
|
+
│ └── ...
|
|
75
|
+
├── fake_data_dsl.gemspec
|
|
76
|
+
└── Rakefile
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Setting Up the Rust Extension
|
|
82
|
+
|
|
83
|
+
### 3.1 Create the Extension Directory
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mkdir -p ext/fake_data_dsl_native/src
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3.2 Create `ext/fake_data_dsl_native/Cargo.toml`
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[package]
|
|
93
|
+
name = "fake_data_dsl_native"
|
|
94
|
+
version = "0.1.0"
|
|
95
|
+
edition = "2021"
|
|
96
|
+
authors = ["Your Name <your.email@example.com>"]
|
|
97
|
+
license = "MIT"
|
|
98
|
+
publish = false
|
|
99
|
+
|
|
100
|
+
[lib]
|
|
101
|
+
crate-type = ["cdylib"]
|
|
102
|
+
|
|
103
|
+
[dependencies]
|
|
104
|
+
# Ruby bindings
|
|
105
|
+
magnus = "0.6"
|
|
106
|
+
rb-sys = "0.9"
|
|
107
|
+
|
|
108
|
+
# Fake data generation (like Ruby's Faker)
|
|
109
|
+
fake = { version = "4", features = [
|
|
110
|
+
"derive",
|
|
111
|
+
"chrono",
|
|
112
|
+
"uuid",
|
|
113
|
+
"random_color",
|
|
114
|
+
"http"
|
|
115
|
+
]}
|
|
116
|
+
|
|
117
|
+
# Random number generation
|
|
118
|
+
rand = "0.8"
|
|
119
|
+
rand_chacha = "0.3"
|
|
120
|
+
|
|
121
|
+
# Parallel processing (optional, for multi-core)
|
|
122
|
+
rayon = "1.8"
|
|
123
|
+
|
|
124
|
+
# JSON handling
|
|
125
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
126
|
+
serde_json = "1.0"
|
|
127
|
+
|
|
128
|
+
# Date/time
|
|
129
|
+
chrono = "0.4"
|
|
130
|
+
|
|
131
|
+
# UUID
|
|
132
|
+
uuid = { version = "1.0", features = ["v4", "v1", "serde"] }
|
|
133
|
+
|
|
134
|
+
[profile.release]
|
|
135
|
+
opt-level = 3
|
|
136
|
+
lto = true
|
|
137
|
+
codegen-units = 1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3.3 Create `ext/fake_data_dsl_native/extconf.rb`
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# frozen_string_literal: true
|
|
144
|
+
|
|
145
|
+
require "mkmf"
|
|
146
|
+
require "rb_sys/mkmf"
|
|
147
|
+
|
|
148
|
+
create_rust_makefile("fake_data_dsl_native") do |r|
|
|
149
|
+
# Release mode for best performance
|
|
150
|
+
r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :release).to_sym
|
|
151
|
+
|
|
152
|
+
# Extra features
|
|
153
|
+
r.extra_cargo_args = ["--features", "parallel"]
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 3.4 Update `Rakefile`
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "bundler/gem_tasks"
|
|
161
|
+
require "rspec/core/rake_task"
|
|
162
|
+
require "rb_sys/extensiontask"
|
|
163
|
+
|
|
164
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
165
|
+
|
|
166
|
+
# Native extension build task
|
|
167
|
+
RbSys::ExtensionTask.new("fake_data_dsl_native", GEMSPEC) do |ext|
|
|
168
|
+
ext.lib_dir = "lib/fake_data_dsl"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
task build: :compile
|
|
172
|
+
task default: [:compile, :spec]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 3.5 Update `fake_data_dsl.gemspec`
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Gem::Specification.new do |spec|
|
|
179
|
+
spec.name = "fake_data_dsl"
|
|
180
|
+
spec.version = FakeDataDSL::VERSION
|
|
181
|
+
# ... other fields ...
|
|
182
|
+
|
|
183
|
+
# Add extension
|
|
184
|
+
spec.extensions = ["ext/fake_data_dsl_native/extconf.rb"]
|
|
185
|
+
|
|
186
|
+
# Dependencies for building
|
|
187
|
+
spec.add_dependency "rb_sys", "~> 0.9"
|
|
188
|
+
|
|
189
|
+
# Include Rust files
|
|
190
|
+
spec.files += Dir["ext/**/*.{rs,toml,rb}"]
|
|
191
|
+
|
|
192
|
+
# Metadata
|
|
193
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 4. Writing the Rust Code
|
|
200
|
+
|
|
201
|
+
### 4.1 Main Entry Point: `src/lib.rs`
|
|
202
|
+
|
|
203
|
+
```rust
|
|
204
|
+
use magnus::{define_module, function, prelude::*, Error, RArray, Ruby, Value};
|
|
205
|
+
|
|
206
|
+
mod generator;
|
|
207
|
+
mod schema;
|
|
208
|
+
mod types;
|
|
209
|
+
|
|
210
|
+
/// Generate records and return as Ruby Array of Hashes
|
|
211
|
+
fn generate(
|
|
212
|
+
ruby: &Ruby,
|
|
213
|
+
schema_spec: RArray,
|
|
214
|
+
count: usize,
|
|
215
|
+
seed: Option<u64>,
|
|
216
|
+
locale: Option<String>,
|
|
217
|
+
) -> Result<RArray, Error> {
|
|
218
|
+
let generator = generator::NativeGenerator::from_ruby(schema_spec, seed, locale)?;
|
|
219
|
+
generator.generate_to_ruby(ruby, count)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Generate records directly to a file (most efficient)
|
|
223
|
+
fn generate_to_file(
|
|
224
|
+
schema_spec: RArray,
|
|
225
|
+
count: usize,
|
|
226
|
+
path: String,
|
|
227
|
+
seed: Option<u64>,
|
|
228
|
+
locale: Option<String>,
|
|
229
|
+
format: Option<String>,
|
|
230
|
+
) -> Result<usize, Error> {
|
|
231
|
+
let generator = generator::NativeGenerator::from_ruby(schema_spec, seed, locale)?;
|
|
232
|
+
let fmt = format.as_deref().unwrap_or("jsonl");
|
|
233
|
+
generator.generate_to_file(count, &path, fmt)
|
|
234
|
+
.map_err(|e| Error::new(ruby_exception(), e.to_string()))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Generate a single record (for testing)
|
|
238
|
+
fn generate_one(
|
|
239
|
+
ruby: &Ruby,
|
|
240
|
+
schema_spec: RArray,
|
|
241
|
+
seed: Option<u64>,
|
|
242
|
+
locale: Option<String>,
|
|
243
|
+
) -> Result<Value, Error> {
|
|
244
|
+
let generator = generator::NativeGenerator::from_ruby(schema_spec, seed, locale)?;
|
|
245
|
+
generator.generate_one_to_ruby(ruby)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Get list of available types
|
|
249
|
+
fn available_types() -> Vec<String> {
|
|
250
|
+
types::AVAILABLE_TYPES.iter().map(|s| s.to_string()).collect()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Get list of available locales
|
|
254
|
+
fn available_locales() -> Vec<String> {
|
|
255
|
+
vec![
|
|
256
|
+
"en", "fr_fr", "de_de", "zh_cn", "zh_tw",
|
|
257
|
+
"ja_jp", "pt_br", "it_it", "nl_nl", "ar_sa"
|
|
258
|
+
].iter().map(|s| s.to_string()).collect()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Get native extension version
|
|
262
|
+
fn version() -> String {
|
|
263
|
+
env!("CARGO_PKG_VERSION").to_string()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Check if extension is loaded
|
|
267
|
+
fn loaded() -> bool {
|
|
268
|
+
true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn ruby_exception() -> magnus::ExceptionClass {
|
|
272
|
+
magnus::exception::runtime_error()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[magnus::init]
|
|
276
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
277
|
+
let module = ruby.define_module("FakeDataDSL")?;
|
|
278
|
+
let native = module.define_module("Native")?;
|
|
279
|
+
|
|
280
|
+
native.define_singleton_method("generate", function!(generate, 4))?;
|
|
281
|
+
native.define_singleton_method("generate_to_file", function!(generate_to_file, 6))?;
|
|
282
|
+
native.define_singleton_method("generate_one", function!(generate_one, 3))?;
|
|
283
|
+
native.define_singleton_method("available_types", function!(available_types, 0))?;
|
|
284
|
+
native.define_singleton_method("available_locales", function!(available_locales, 0))?;
|
|
285
|
+
native.define_singleton_method("version", function!(version, 0))?;
|
|
286
|
+
native.define_singleton_method("loaded?", function!(loaded, 0))?;
|
|
287
|
+
|
|
288
|
+
Ok(())
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 4.2 Type Generators: `src/types.rs`
|
|
293
|
+
|
|
294
|
+
```rust
|
|
295
|
+
use fake::faker::address::raw::*;
|
|
296
|
+
use fake::faker::company::raw::*;
|
|
297
|
+
use fake::faker::internet::raw::*;
|
|
298
|
+
use fake::faker::lorem::raw::*;
|
|
299
|
+
use fake::faker::name::raw::*;
|
|
300
|
+
use fake::faker::phone_number::raw::*;
|
|
301
|
+
use fake::locales::*;
|
|
302
|
+
use fake::Fake;
|
|
303
|
+
use rand::Rng;
|
|
304
|
+
use serde_json::{json, Value};
|
|
305
|
+
use uuid::Uuid;
|
|
306
|
+
|
|
307
|
+
/// List of all supported type names
|
|
308
|
+
pub static AVAILABLE_TYPES: &[&str] = &[
|
|
309
|
+
// Personal
|
|
310
|
+
"name", "first_name", "last_name", "full_name", "title",
|
|
311
|
+
// Internet
|
|
312
|
+
"email", "username", "password", "url", "ip", "ipv4", "ipv6",
|
|
313
|
+
"mac_address", "user_agent", "domain",
|
|
314
|
+
// Address
|
|
315
|
+
"city", "country", "country_code", "street", "street_address",
|
|
316
|
+
"postal_code", "zip_code", "latitude", "longitude", "state",
|
|
317
|
+
// Company
|
|
318
|
+
"company_name", "company", "catch_phrase", "buzzword", "industry",
|
|
319
|
+
// Text
|
|
320
|
+
"word", "words", "sentence", "sentences", "paragraph", "paragraphs", "text",
|
|
321
|
+
// Phone
|
|
322
|
+
"phone", "phone_number", "cell_phone",
|
|
323
|
+
// Date/Time
|
|
324
|
+
"date", "time", "datetime", "timestamp", "past_date", "future_date",
|
|
325
|
+
// Identifiers
|
|
326
|
+
"uuid", "uuid_v4", "uuid_v1", "id_sequence",
|
|
327
|
+
// Finance
|
|
328
|
+
"credit_card", "currency_code", "bic",
|
|
329
|
+
// Primitives
|
|
330
|
+
"number", "integer", "float", "boolean", "bool",
|
|
331
|
+
// Color
|
|
332
|
+
"hex_color", "rgb_color",
|
|
333
|
+
// Special
|
|
334
|
+
"enum", "const", "array",
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
/// Locale enum matching fake-rs locales
|
|
338
|
+
#[derive(Clone, Copy, Debug)]
|
|
339
|
+
pub enum Locale {
|
|
340
|
+
EN,
|
|
341
|
+
FR,
|
|
342
|
+
DE,
|
|
343
|
+
ZH_CN,
|
|
344
|
+
ZH_TW,
|
|
345
|
+
JA,
|
|
346
|
+
PT_BR,
|
|
347
|
+
IT,
|
|
348
|
+
NL,
|
|
349
|
+
AR,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
impl Locale {
|
|
353
|
+
pub fn from_string(s: &str) -> Self {
|
|
354
|
+
match s.to_lowercase().as_str() {
|
|
355
|
+
"fr" | "fr_fr" | "french" => Locale::FR,
|
|
356
|
+
"de" | "de_de" | "german" => Locale::DE,
|
|
357
|
+
"zh_cn" | "chinese" | "chinese_simplified" => Locale::ZH_CN,
|
|
358
|
+
"zh_tw" | "chinese_traditional" => Locale::ZH_TW,
|
|
359
|
+
"ja" | "ja_jp" | "japanese" => Locale::JA,
|
|
360
|
+
"pt_br" | "portuguese" | "brazilian" => Locale::PT_BR,
|
|
361
|
+
"it" | "it_it" | "italian" => Locale::IT,
|
|
362
|
+
"nl" | "nl_nl" | "dutch" => Locale::NL,
|
|
363
|
+
"ar" | "ar_sa" | "arabic" => Locale::AR,
|
|
364
|
+
_ => Locale::EN,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Generate a value for a given type
|
|
370
|
+
pub fn generate_value<R: Rng>(
|
|
371
|
+
rng: &mut R,
|
|
372
|
+
type_name: &str,
|
|
373
|
+
args: &Value,
|
|
374
|
+
locale: Locale,
|
|
375
|
+
index: usize,
|
|
376
|
+
) -> Value {
|
|
377
|
+
match type_name {
|
|
378
|
+
// ========== Personal Names ==========
|
|
379
|
+
"name" | "full_name" => match locale {
|
|
380
|
+
Locale::EN => json!(Name(EN).fake_with_rng::<String, _>(rng)),
|
|
381
|
+
Locale::FR => json!(Name(FR_FR).fake_with_rng::<String, _>(rng)),
|
|
382
|
+
Locale::DE => json!(Name(DE_DE).fake_with_rng::<String, _>(rng)),
|
|
383
|
+
Locale::ZH_CN => json!(Name(ZH_CN).fake_with_rng::<String, _>(rng)),
|
|
384
|
+
Locale::ZH_TW => json!(Name(ZH_TW).fake_with_rng::<String, _>(rng)),
|
|
385
|
+
Locale::JA => json!(Name(JA_JP).fake_with_rng::<String, _>(rng)),
|
|
386
|
+
Locale::PT_BR => json!(Name(PT_BR).fake_with_rng::<String, _>(rng)),
|
|
387
|
+
Locale::IT => json!(Name(IT_IT).fake_with_rng::<String, _>(rng)),
|
|
388
|
+
Locale::NL => json!(Name(NL_NL).fake_with_rng::<String, _>(rng)),
|
|
389
|
+
Locale::AR => json!(Name(AR_SA).fake_with_rng::<String, _>(rng)),
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
"first_name" => match locale {
|
|
393
|
+
Locale::EN => json!(FirstName(EN).fake_with_rng::<String, _>(rng)),
|
|
394
|
+
Locale::FR => json!(FirstName(FR_FR).fake_with_rng::<String, _>(rng)),
|
|
395
|
+
Locale::DE => json!(FirstName(DE_DE).fake_with_rng::<String, _>(rng)),
|
|
396
|
+
Locale::ZH_CN => json!(FirstName(ZH_CN).fake_with_rng::<String, _>(rng)),
|
|
397
|
+
Locale::ZH_TW => json!(FirstName(ZH_TW).fake_with_rng::<String, _>(rng)),
|
|
398
|
+
Locale::JA => json!(FirstName(JA_JP).fake_with_rng::<String, _>(rng)),
|
|
399
|
+
_ => json!(FirstName(EN).fake_with_rng::<String, _>(rng)),
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
"last_name" => match locale {
|
|
403
|
+
Locale::EN => json!(LastName(EN).fake_with_rng::<String, _>(rng)),
|
|
404
|
+
Locale::FR => json!(LastName(FR_FR).fake_with_rng::<String, _>(rng)),
|
|
405
|
+
Locale::DE => json!(LastName(DE_DE).fake_with_rng::<String, _>(rng)),
|
|
406
|
+
Locale::ZH_CN => json!(LastName(ZH_CN).fake_with_rng::<String, _>(rng)),
|
|
407
|
+
Locale::ZH_TW => json!(LastName(ZH_TW).fake_with_rng::<String, _>(rng)),
|
|
408
|
+
Locale::JA => json!(LastName(JA_JP).fake_with_rng::<String, _>(rng)),
|
|
409
|
+
_ => json!(LastName(EN).fake_with_rng::<String, _>(rng)),
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
"title" => json!(Title(EN).fake_with_rng::<String, _>(rng)),
|
|
413
|
+
|
|
414
|
+
// ========== Internet ==========
|
|
415
|
+
"email" => {
|
|
416
|
+
let base: String = FreeEmail(EN).fake_with_rng(rng);
|
|
417
|
+
// Make email unique by adding index
|
|
418
|
+
let parts: Vec<&str> = base.splitn(2, '@').collect();
|
|
419
|
+
if parts.len() == 2 {
|
|
420
|
+
json!(format!("{}{}@{}", parts[0], index, parts[1]))
|
|
421
|
+
} else {
|
|
422
|
+
json!(base)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
"username" => json!(Username(EN).fake_with_rng::<String, _>(rng)),
|
|
427
|
+
|
|
428
|
+
"password" => {
|
|
429
|
+
let min = args.get("min").and_then(|v| v.as_u64()).unwrap_or(8) as usize;
|
|
430
|
+
let max = args.get("max").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
|
431
|
+
json!(Password(EN, min..max).fake_with_rng::<String, _>(rng))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
"url" => {
|
|
435
|
+
let domain: String = DomainSuffix(EN).fake_with_rng(rng);
|
|
436
|
+
json!(format!("https://www.{}.{}",
|
|
437
|
+
Word(EN).fake_with_rng::<String, _>(rng).to_lowercase(),
|
|
438
|
+
domain
|
|
439
|
+
))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
"ip" | "ipv4" => json!(IPv4(EN).fake_with_rng::<String, _>(rng)),
|
|
443
|
+
"ipv6" => json!(IPv6(EN).fake_with_rng::<String, _>(rng)),
|
|
444
|
+
"mac_address" => json!(MACAddress(EN).fake_with_rng::<String, _>(rng)),
|
|
445
|
+
"user_agent" => json!(UserAgent(EN).fake_with_rng::<String, _>(rng)),
|
|
446
|
+
"domain" => json!(DomainSuffix(EN).fake_with_rng::<String, _>(rng)),
|
|
447
|
+
|
|
448
|
+
// ========== Address ==========
|
|
449
|
+
"city" => match locale {
|
|
450
|
+
Locale::EN => json!(CityName(EN).fake_with_rng::<String, _>(rng)),
|
|
451
|
+
Locale::FR => json!(CityName(FR_FR).fake_with_rng::<String, _>(rng)),
|
|
452
|
+
Locale::DE => json!(CityName(DE_DE).fake_with_rng::<String, _>(rng)),
|
|
453
|
+
_ => json!(CityName(EN).fake_with_rng::<String, _>(rng)),
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
"country" => json!(CountryName(EN).fake_with_rng::<String, _>(rng)),
|
|
457
|
+
"country_code" => json!(CountryCode(EN).fake_with_rng::<String, _>(rng)),
|
|
458
|
+
|
|
459
|
+
"street" | "street_address" => match locale {
|
|
460
|
+
Locale::EN => json!(StreetName(EN).fake_with_rng::<String, _>(rng)),
|
|
461
|
+
Locale::FR => json!(StreetName(FR_FR).fake_with_rng::<String, _>(rng)),
|
|
462
|
+
_ => json!(StreetName(EN).fake_with_rng::<String, _>(rng)),
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
"postal_code" | "zip_code" => json!(ZipCode(EN).fake_with_rng::<String, _>(rng)),
|
|
466
|
+
|
|
467
|
+
"latitude" => {
|
|
468
|
+
let lat: f64 = Latitude(EN).fake_with_rng(rng);
|
|
469
|
+
json!(lat)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
"longitude" => {
|
|
473
|
+
let lng: f64 = Longitude(EN).fake_with_rng(rng);
|
|
474
|
+
json!(lng)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
"state" => json!(StateName(EN).fake_with_rng::<String, _>(rng)),
|
|
478
|
+
|
|
479
|
+
// ========== Company ==========
|
|
480
|
+
"company" | "company_name" => json!(CompanyName(EN).fake_with_rng::<String, _>(rng)),
|
|
481
|
+
"catch_phrase" => json!(CatchPhrase(EN).fake_with_rng::<String, _>(rng)),
|
|
482
|
+
"buzzword" => json!(Buzzword(EN).fake_with_rng::<String, _>(rng)),
|
|
483
|
+
"industry" => json!(Industry(EN).fake_with_rng::<String, _>(rng)),
|
|
484
|
+
|
|
485
|
+
// ========== Text ==========
|
|
486
|
+
"word" => json!(Word(EN).fake_with_rng::<String, _>(rng)),
|
|
487
|
+
|
|
488
|
+
"words" => {
|
|
489
|
+
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
|
490
|
+
let words: Vec<String> = Words(EN, count..count + 1).fake_with_rng(rng);
|
|
491
|
+
json!(words.join(" "))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
"sentence" => {
|
|
495
|
+
let words = args.get("words").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
|
496
|
+
json!(Sentence(EN, words..words + 5).fake_with_rng::<String, _>(rng))
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
"sentences" => {
|
|
500
|
+
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
|
501
|
+
json!(Sentences(EN, count..count + 2).fake_with_rng::<Vec<String>, _>(rng).join(" "))
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
"paragraph" => {
|
|
505
|
+
let sentences = args.get("sentences").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
|
506
|
+
json!(Paragraph(EN, sentences..sentences + 3).fake_with_rng::<String, _>(rng))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
"paragraphs" => {
|
|
510
|
+
let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
|
511
|
+
json!(Paragraphs(EN, count..count + 2).fake_with_rng::<Vec<String>, _>(rng).join("\n\n"))
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
"text" => {
|
|
515
|
+
let min = args.get("min").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
|
|
516
|
+
let max = args.get("max").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
|
517
|
+
let len = rng.gen_range(min..=max);
|
|
518
|
+
let text: String = (0..len)
|
|
519
|
+
.map(|_| rng.gen_range(b'a'..=b'z') as char)
|
|
520
|
+
.collect();
|
|
521
|
+
json!(text)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ========== Phone ==========
|
|
525
|
+
"phone" | "phone_number" => json!(PhoneNumber(EN).fake_with_rng::<String, _>(rng)),
|
|
526
|
+
"cell_phone" => json!(CellNumber(EN).fake_with_rng::<String, _>(rng)),
|
|
527
|
+
|
|
528
|
+
// ========== Date/Time ==========
|
|
529
|
+
"date" => {
|
|
530
|
+
let days_ago = rng.gen_range(0i64..365);
|
|
531
|
+
let date = chrono::Utc::now() - chrono::Duration::days(days_ago);
|
|
532
|
+
json!(date.format("%Y-%m-%d").to_string())
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
"time" => {
|
|
536
|
+
let hour = rng.gen_range(0..24);
|
|
537
|
+
let minute = rng.gen_range(0..60);
|
|
538
|
+
let second = rng.gen_range(0..60);
|
|
539
|
+
json!(format!("{:02}:{:02}:{:02}", hour, minute, second))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
"datetime" | "timestamp" => {
|
|
543
|
+
let days_ago = rng.gen_range(0i64..365);
|
|
544
|
+
let dt = chrono::Utc::now() - chrono::Duration::days(days_ago);
|
|
545
|
+
json!(dt.to_rfc3339())
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
"past_date" => {
|
|
549
|
+
let days_ago = rng.gen_range(1i64..3650);
|
|
550
|
+
let date = chrono::Utc::now() - chrono::Duration::days(days_ago);
|
|
551
|
+
json!(date.format("%Y-%m-%d").to_string())
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
"future_date" => {
|
|
555
|
+
let days_ahead = rng.gen_range(1i64..365);
|
|
556
|
+
let date = chrono::Utc::now() + chrono::Duration::days(days_ahead);
|
|
557
|
+
json!(date.format("%Y-%m-%d").to_string())
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ========== Identifiers ==========
|
|
561
|
+
"uuid" | "uuid_v4" => json!(Uuid::new_v4().to_string()),
|
|
562
|
+
|
|
563
|
+
"uuid_v1" => {
|
|
564
|
+
// Sequential UUID-like format
|
|
565
|
+
json!(format!(
|
|
566
|
+
"{:08x}-{:04x}-1000-8000-{:012x}",
|
|
567
|
+
(index >> 32) as u32,
|
|
568
|
+
(index >> 16) as u16,
|
|
569
|
+
index as u64
|
|
570
|
+
))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
"id_sequence" => {
|
|
574
|
+
let start = args.get("start").and_then(|v| v.as_u64()).unwrap_or(1);
|
|
575
|
+
json!(start + index as u64)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ========== Primitives ==========
|
|
579
|
+
"number" | "integer" => {
|
|
580
|
+
let min = args.get("min").and_then(|v| v.as_i64()).unwrap_or(0);
|
|
581
|
+
let max = args.get("max").and_then(|v| v.as_i64()).unwrap_or(100);
|
|
582
|
+
json!(rng.gen_range(min..=max))
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
"float" => {
|
|
586
|
+
let min = args.get("min").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
587
|
+
let max = args.get("max").and_then(|v| v.as_f64()).unwrap_or(100.0);
|
|
588
|
+
let precision = args.get("precision").and_then(|v| v.as_u64()).unwrap_or(2) as i32;
|
|
589
|
+
let val = rng.gen_range(min..=max);
|
|
590
|
+
let multiplier = 10_f64.powi(precision);
|
|
591
|
+
json!((val * multiplier).round() / multiplier)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
"boolean" | "bool" => {
|
|
595
|
+
let prob = args
|
|
596
|
+
.get("probability")
|
|
597
|
+
.or_else(|| args.get("true_probability"))
|
|
598
|
+
.and_then(|v| v.as_f64())
|
|
599
|
+
.unwrap_or(0.5);
|
|
600
|
+
json!(rng.gen_bool(prob))
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ========== Color ==========
|
|
604
|
+
"hex_color" => {
|
|
605
|
+
use fake::faker::color::raw::HexColor;
|
|
606
|
+
json!(HexColor(EN).fake_with_rng::<String, _>(rng))
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
"rgb_color" => {
|
|
610
|
+
let r = rng.gen_range(0..256);
|
|
611
|
+
let g = rng.gen_range(0..256);
|
|
612
|
+
let b = rng.gen_range(0..256);
|
|
613
|
+
json!(format!("rgb({}, {}, {})", r, g, b))
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ========== Special Types ==========
|
|
617
|
+
"enum" => {
|
|
618
|
+
if let Some(values) = args.get("values").and_then(|v| v.as_array()) {
|
|
619
|
+
if !values.is_empty() {
|
|
620
|
+
return values[rng.gen_range(0..values.len())].clone();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
Value::Null
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
"const" => args.get("value").cloned().unwrap_or(Value::Null),
|
|
627
|
+
|
|
628
|
+
"array" => {
|
|
629
|
+
let item_type = args.get("type").and_then(|v| v.as_str()).unwrap_or("word");
|
|
630
|
+
let min = args.get("min").and_then(|v| v.as_u64()).unwrap_or(1) as usize;
|
|
631
|
+
let max = args.get("max").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
|
632
|
+
let count = rng.gen_range(min..=max);
|
|
633
|
+
|
|
634
|
+
let items: Vec<Value> = (0..count)
|
|
635
|
+
.map(|i| generate_value(rng, item_type, &json!({}), locale, index + i))
|
|
636
|
+
.collect();
|
|
637
|
+
json!(items)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Unknown type
|
|
641
|
+
_ => Value::Null,
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### 4.3 Generator Engine: `src/generator.rs`
|
|
647
|
+
|
|
648
|
+
```rust
|
|
649
|
+
use magnus::{Error, RArray, RHash, Ruby, Symbol, Value};
|
|
650
|
+
use rand::SeedableRng;
|
|
651
|
+
use rand_chacha::ChaCha8Rng;
|
|
652
|
+
use serde_json::Map;
|
|
653
|
+
use std::fs::File;
|
|
654
|
+
use std::io::{BufWriter, Write};
|
|
655
|
+
|
|
656
|
+
use crate::types::{generate_value, Locale};
|
|
657
|
+
|
|
658
|
+
/// Field specification parsed from Ruby
|
|
659
|
+
#[derive(Clone)]
|
|
660
|
+
pub struct FieldSpec {
|
|
661
|
+
pub name: String,
|
|
662
|
+
pub type_name: String,
|
|
663
|
+
pub args: serde_json::Value,
|
|
664
|
+
pub optional: bool,
|
|
665
|
+
pub nullable: bool,
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/// Native generator engine
|
|
669
|
+
pub struct NativeGenerator {
|
|
670
|
+
fields: Vec<FieldSpec>,
|
|
671
|
+
seed: Option<u64>,
|
|
672
|
+
locale: Locale,
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
impl NativeGenerator {
|
|
676
|
+
/// Create generator from Ruby schema specification
|
|
677
|
+
///
|
|
678
|
+
/// Expected format: Array of [name, type_name, args_json, optional, nullable]
|
|
679
|
+
pub fn from_ruby(schema_spec: RArray, seed: Option<u64>, locale: Option<String>) -> Result<Self, Error> {
|
|
680
|
+
let mut fields = Vec::new();
|
|
681
|
+
|
|
682
|
+
for item in schema_spec.each() {
|
|
683
|
+
let item = item?;
|
|
684
|
+
let field_array: RArray = item.try_convert()?;
|
|
685
|
+
|
|
686
|
+
let name: String = field_array.entry::<String>(0)?;
|
|
687
|
+
let type_name: String = field_array.entry::<String>(1)?;
|
|
688
|
+
let args_json: String = field_array.entry::<String>(2)?;
|
|
689
|
+
let optional: bool = field_array.entry::<bool>(3).unwrap_or(false);
|
|
690
|
+
let nullable: bool = field_array.entry::<bool>(4).unwrap_or(false);
|
|
691
|
+
|
|
692
|
+
let args: serde_json::Value = serde_json::from_str(&args_json)
|
|
693
|
+
.unwrap_or(serde_json::Value::Object(Map::new()));
|
|
694
|
+
|
|
695
|
+
fields.push(FieldSpec {
|
|
696
|
+
name,
|
|
697
|
+
type_name,
|
|
698
|
+
args,
|
|
699
|
+
optional,
|
|
700
|
+
nullable,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let loc = locale.map(|s| Locale::from_string(&s)).unwrap_or(Locale::EN);
|
|
705
|
+
|
|
706
|
+
Ok(Self {
|
|
707
|
+
fields,
|
|
708
|
+
seed,
|
|
709
|
+
locale: loc,
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/// Generate records and return as Ruby Array of Hashes
|
|
714
|
+
pub fn generate_to_ruby(&self, ruby: &Ruby, count: usize) -> Result<RArray, Error> {
|
|
715
|
+
let result = ruby.ary_new_capa(count);
|
|
716
|
+
let mut rng = self.create_rng(0);
|
|
717
|
+
|
|
718
|
+
for i in 0..count {
|
|
719
|
+
let record = self.generate_record(&mut rng, i)?;
|
|
720
|
+
let hash = self.value_to_ruby_hash(ruby, &record)?;
|
|
721
|
+
result.push(hash)?;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
Ok(result)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/// Generate a single record to Ruby Hash
|
|
728
|
+
pub fn generate_one_to_ruby(&self, ruby: &Ruby) -> Result<Value, Error> {
|
|
729
|
+
let mut rng = self.create_rng(0);
|
|
730
|
+
let record = self.generate_record(&mut rng, 0)?;
|
|
731
|
+
self.value_to_ruby_hash(ruby, &record)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/// Generate records directly to file (most efficient)
|
|
735
|
+
pub fn generate_to_file(&self, count: usize, path: &str, format: &str) -> std::io::Result<usize> {
|
|
736
|
+
let file = File::create(path)?;
|
|
737
|
+
let mut writer = BufWriter::with_capacity(64 * 1024 * 1024, file); // 64MB buffer
|
|
738
|
+
let mut rng = self.create_rng(0);
|
|
739
|
+
|
|
740
|
+
match format {
|
|
741
|
+
"csv" => {
|
|
742
|
+
// Write header
|
|
743
|
+
let headers: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
|
|
744
|
+
writeln!(writer, "{}", headers.join(","))?;
|
|
745
|
+
|
|
746
|
+
// Write rows
|
|
747
|
+
for i in 0..count {
|
|
748
|
+
let record = self.generate_record(&mut rng, i)?;
|
|
749
|
+
let values: Vec<String> = self.fields.iter().map(|f| {
|
|
750
|
+
match record.get(&f.name) {
|
|
751
|
+
Some(serde_json::Value::String(s)) => {
|
|
752
|
+
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
|
753
|
+
format!("\"{}\"", s.replace('"', "\"\""))
|
|
754
|
+
} else {
|
|
755
|
+
s.clone()
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
Some(v) => v.to_string(),
|
|
759
|
+
None => String::new(),
|
|
760
|
+
}
|
|
761
|
+
}).collect();
|
|
762
|
+
writeln!(writer, "{}", values.join(","))?;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
"json" => {
|
|
766
|
+
// Write as JSON array
|
|
767
|
+
write!(writer, "[")?;
|
|
768
|
+
for i in 0..count {
|
|
769
|
+
if i > 0 {
|
|
770
|
+
write!(writer, ",")?;
|
|
771
|
+
}
|
|
772
|
+
let record = self.generate_record(&mut rng, i)?;
|
|
773
|
+
write!(writer, "{}", serde_json::to_string(&record).unwrap())?;
|
|
774
|
+
}
|
|
775
|
+
writeln!(writer, "]")?;
|
|
776
|
+
}
|
|
777
|
+
_ => {
|
|
778
|
+
// Default: JSONL (one JSON object per line)
|
|
779
|
+
for i in 0..count {
|
|
780
|
+
let record = self.generate_record(&mut rng, i)?;
|
|
781
|
+
writeln!(writer, "{}", serde_json::to_string(&record).unwrap())?;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
writer.flush()?;
|
|
787
|
+
Ok(count)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/// Generate a single record as JSON object
|
|
791
|
+
fn generate_record(&self, rng: &mut ChaCha8Rng, index: usize) -> std::io::Result<Map<String, serde_json::Value>> {
|
|
792
|
+
let mut record = Map::new();
|
|
793
|
+
|
|
794
|
+
for field in &self.fields {
|
|
795
|
+
// Handle optional fields
|
|
796
|
+
if field.optional && rng.gen_bool(0.2) {
|
|
797
|
+
continue; // Skip 20% of optional fields
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Handle nullable fields
|
|
801
|
+
if field.nullable && rng.gen_bool(0.1) {
|
|
802
|
+
record.insert(field.name.clone(), serde_json::Value::Null);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
let value = generate_value(rng, &field.type_name, &field.args, self.locale, index);
|
|
807
|
+
record.insert(field.name.clone(), value);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
Ok(record)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/// Create RNG from seed
|
|
814
|
+
fn create_rng(&self, offset: u64) -> ChaCha8Rng {
|
|
815
|
+
match self.seed {
|
|
816
|
+
Some(seed) => ChaCha8Rng::seed_from_u64(seed.wrapping_add(offset)),
|
|
817
|
+
None => ChaCha8Rng::from_entropy(),
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/// Convert serde_json Value to Ruby Value
|
|
822
|
+
fn value_to_ruby_hash(&self, ruby: &Ruby, record: &Map<String, serde_json::Value>) -> Result<Value, Error> {
|
|
823
|
+
let hash = ruby.hash_new();
|
|
824
|
+
|
|
825
|
+
for (key, value) in record {
|
|
826
|
+
let ruby_key = Symbol::new(key);
|
|
827
|
+
let ruby_value = self.json_to_ruby(ruby, value)?;
|
|
828
|
+
hash.aset(ruby_key, ruby_value)?;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
Ok(hash.as_value())
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/// Convert serde_json Value to Ruby Value
|
|
835
|
+
fn json_to_ruby(&self, ruby: &Ruby, value: &serde_json::Value) -> Result<Value, Error> {
|
|
836
|
+
match value {
|
|
837
|
+
serde_json::Value::Null => Ok(ruby.qnil().as_value()),
|
|
838
|
+
serde_json::Value::Bool(b) => Ok(if *b { ruby.qtrue() } else { ruby.qfalse() }.as_value()),
|
|
839
|
+
serde_json::Value::Number(n) => {
|
|
840
|
+
if let Some(i) = n.as_i64() {
|
|
841
|
+
Ok(ruby.integer_from_i64(i).as_value())
|
|
842
|
+
} else if let Some(f) = n.as_f64() {
|
|
843
|
+
Ok(ruby.float_from_f64(f).as_value())
|
|
844
|
+
} else {
|
|
845
|
+
Ok(ruby.qnil().as_value())
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
serde_json::Value::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
849
|
+
serde_json::Value::Array(arr) => {
|
|
850
|
+
let ruby_arr = ruby.ary_new_capa(arr.len());
|
|
851
|
+
for item in arr {
|
|
852
|
+
ruby_arr.push(self.json_to_ruby(ruby, item)?)?;
|
|
853
|
+
}
|
|
854
|
+
Ok(ruby_arr.as_value())
|
|
855
|
+
}
|
|
856
|
+
serde_json::Value::Object(obj) => {
|
|
857
|
+
let ruby_hash = ruby.hash_new();
|
|
858
|
+
for (k, v) in obj {
|
|
859
|
+
ruby_hash.aset(Symbol::new(k), self.json_to_ruby(ruby, v)?)?;
|
|
860
|
+
}
|
|
861
|
+
Ok(ruby_hash.as_value())
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### 4.4 Schema Parser: `src/schema.rs`
|
|
869
|
+
|
|
870
|
+
```rust
|
|
871
|
+
// Optional: Add schema parsing if needed
|
|
872
|
+
// For now, the Ruby side handles parsing and passes field specs
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
## 5. Building the Extension
|
|
878
|
+
|
|
879
|
+
### 5.1 Build Commands
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
# Install dependencies
|
|
883
|
+
bundle install
|
|
884
|
+
|
|
885
|
+
# Compile the native extension
|
|
886
|
+
bundle exec rake compile
|
|
887
|
+
|
|
888
|
+
# Or build manually
|
|
889
|
+
cd ext/fake_data_dsl_native
|
|
890
|
+
cargo build --release
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### 5.2 Development Workflow
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
# Build and test
|
|
897
|
+
bundle exec rake compile
|
|
898
|
+
bundle exec rspec
|
|
899
|
+
|
|
900
|
+
# Clean and rebuild
|
|
901
|
+
bundle exec rake clobber
|
|
902
|
+
bundle exec rake compile
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
## 6. Integrating with Ruby
|
|
908
|
+
|
|
909
|
+
### 6.1 Create `lib/fake_data_dsl/native_engine.rb`
|
|
910
|
+
|
|
911
|
+
```ruby
|
|
912
|
+
# frozen_string_literal: true
|
|
913
|
+
|
|
914
|
+
module FakeDataDSL
|
|
915
|
+
# High-performance native engine using Rust + fake-rs
|
|
916
|
+
class NativeEngine
|
|
917
|
+
class << self
|
|
918
|
+
# Check if native extension is available
|
|
919
|
+
def available?
|
|
920
|
+
@available ||= begin
|
|
921
|
+
require "fake_data_dsl/fake_data_dsl_native"
|
|
922
|
+
defined?(FakeDataDSL::Native) && Native.respond_to?(:loaded?) && Native.loaded?
|
|
923
|
+
rescue LoadError
|
|
924
|
+
false
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# Get available types in native engine
|
|
929
|
+
def available_types
|
|
930
|
+
return [] unless available?
|
|
931
|
+
Native.available_types
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Get available locales in native engine
|
|
935
|
+
def available_locales
|
|
936
|
+
return [] unless available?
|
|
937
|
+
Native.available_locales
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# Get native extension version
|
|
941
|
+
def version
|
|
942
|
+
return nil unless available?
|
|
943
|
+
Native.version
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
attr_reader :schema, :locale
|
|
948
|
+
|
|
949
|
+
# Initialize with a schema
|
|
950
|
+
# @param schema [FakeDataDSL::Schema] The schema to generate from
|
|
951
|
+
# @param locale [String] Locale for fake data (en, fr_fr, de_de, etc.)
|
|
952
|
+
def initialize(schema, locale: "en")
|
|
953
|
+
unless self.class.available?
|
|
954
|
+
raise LoadError, "Native extension not available. Install Rust and run `bundle exec rake compile`"
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
@schema = schema
|
|
958
|
+
@locale = locale
|
|
959
|
+
@field_specs = build_field_specs
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Generate multiple records
|
|
963
|
+
# @param count [Integer] Number of records to generate
|
|
964
|
+
# @param seed [Integer, nil] Random seed for reproducibility
|
|
965
|
+
# @return [Array<Hash>] Generated records
|
|
966
|
+
def generate_many(count, seed: nil)
|
|
967
|
+
Native.generate(@field_specs, count, seed, @locale)
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
# Generate a single record
|
|
971
|
+
# @param seed [Integer, nil] Random seed for reproducibility
|
|
972
|
+
# @return [Hash] Generated record
|
|
973
|
+
def generate(seed: nil)
|
|
974
|
+
Native.generate_one(@field_specs, seed, @locale)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
# Generate records directly to file
|
|
978
|
+
# @param count [Integer] Number of records to generate
|
|
979
|
+
# @param path [String] Output file path
|
|
980
|
+
# @param seed [Integer, nil] Random seed for reproducibility
|
|
981
|
+
# @param format [String] Output format: jsonl, json, csv
|
|
982
|
+
# @return [Integer] Number of records written
|
|
983
|
+
def generate_to_file(count, path, seed: nil, format: "jsonl")
|
|
984
|
+
Native.generate_to_file(@field_specs, count, path, seed, @locale, format)
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
# Alias for compatibility
|
|
988
|
+
alias_method :to_file, :generate_to_file
|
|
989
|
+
|
|
990
|
+
private
|
|
991
|
+
|
|
992
|
+
# Build field specs for native engine
|
|
993
|
+
# Format: [name, type_name, args_json, optional, nullable]
|
|
994
|
+
def build_field_specs
|
|
995
|
+
@schema.fields.map do |field|
|
|
996
|
+
[
|
|
997
|
+
field.name.to_s,
|
|
998
|
+
map_type_name(field.type_name),
|
|
999
|
+
(field.type_args || {}).to_json,
|
|
1000
|
+
field.optional?,
|
|
1001
|
+
field.nullable?
|
|
1002
|
+
]
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Map DSL type names to native engine type names
|
|
1007
|
+
def map_type_name(type_name)
|
|
1008
|
+
# Most types have the same name
|
|
1009
|
+
case type_name.to_s
|
|
1010
|
+
when "full_name" then "name"
|
|
1011
|
+
when "phone_number" then "phone"
|
|
1012
|
+
when "street_address" then "street"
|
|
1013
|
+
when "zip_code" then "postal_code"
|
|
1014
|
+
else type_name.to_s
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
end
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
### 6.2 Update Main Module `lib/fake_data_dsl.rb`
|
|
1022
|
+
|
|
1023
|
+
```ruby
|
|
1024
|
+
# Add to the module
|
|
1025
|
+
module FakeDataDSL
|
|
1026
|
+
class << self
|
|
1027
|
+
# Check if native engine is available
|
|
1028
|
+
def native_available?
|
|
1029
|
+
NativeEngine.available?
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
# Create native engine for a schema
|
|
1033
|
+
def native_engine(schema, locale: "en")
|
|
1034
|
+
NativeEngine.new(schema, locale: locale)
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# Generate using native engine (fastest)
|
|
1038
|
+
def generate_native(schema_or_dsl, count, seed: nil, locale: "en")
|
|
1039
|
+
schema = schema_or_dsl.is_a?(String) ? parse(schema_or_dsl) : schema_or_dsl
|
|
1040
|
+
engine = NativeEngine.new(schema, locale: locale)
|
|
1041
|
+
engine.generate_many(count, seed: seed)
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
# Generate to file using native engine
|
|
1045
|
+
def generate_native_to_file(schema_or_dsl, count, path, seed: nil, locale: "en", format: "jsonl")
|
|
1046
|
+
schema = schema_or_dsl.is_a?(String) ? parse(schema_or_dsl) : schema_or_dsl
|
|
1047
|
+
engine = NativeEngine.new(schema, locale: locale)
|
|
1048
|
+
engine.generate_to_file(count, path, seed: seed, format: format)
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## 7. Testing
|
|
1057
|
+
|
|
1058
|
+
### 7.1 Create `spec/native_engine_spec.rb`
|
|
1059
|
+
|
|
1060
|
+
```ruby
|
|
1061
|
+
# frozen_string_literal: true
|
|
1062
|
+
|
|
1063
|
+
require "spec_helper"
|
|
1064
|
+
|
|
1065
|
+
RSpec.describe FakeDataDSL::NativeEngine, if: FakeDataDSL::NativeEngine.available? do
|
|
1066
|
+
let(:schema) do
|
|
1067
|
+
FakeDataDSL.parse(<<~DSL)
|
|
1068
|
+
User:
|
|
1069
|
+
id: uuid
|
|
1070
|
+
name: name
|
|
1071
|
+
email: email
|
|
1072
|
+
age: number(18..80)
|
|
1073
|
+
active: boolean
|
|
1074
|
+
DSL
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
describe ".available?" do
|
|
1078
|
+
it "returns true when native extension is loaded" do
|
|
1079
|
+
expect(described_class.available?).to be true
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
describe ".available_types" do
|
|
1084
|
+
it "returns list of supported types" do
|
|
1085
|
+
types = described_class.available_types
|
|
1086
|
+
expect(types).to include("name", "email", "uuid", "number")
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
describe ".available_locales" do
|
|
1091
|
+
it "returns list of supported locales" do
|
|
1092
|
+
locales = described_class.available_locales
|
|
1093
|
+
expect(locales).to include("en", "fr_fr", "de_de")
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
describe "#generate" do
|
|
1098
|
+
subject(:engine) { described_class.new(schema) }
|
|
1099
|
+
|
|
1100
|
+
it "generates a single record" do
|
|
1101
|
+
record = engine.generate(seed: 42)
|
|
1102
|
+
|
|
1103
|
+
expect(record).to be_a(Hash)
|
|
1104
|
+
expect(record[:id]).to match(/^[0-9a-f-]{36}$/)
|
|
1105
|
+
expect(record[:name]).to be_a(String)
|
|
1106
|
+
expect(record[:email]).to include("@")
|
|
1107
|
+
expect(record[:age]).to be_between(18, 80)
|
|
1108
|
+
expect(record[:active]).to be_in([true, false])
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
it "produces deterministic output with seed" do
|
|
1112
|
+
record1 = engine.generate(seed: 42)
|
|
1113
|
+
record2 = engine.generate(seed: 42)
|
|
1114
|
+
|
|
1115
|
+
expect(record1).to eq(record2)
|
|
1116
|
+
end
|
|
1117
|
+
end
|
|
1118
|
+
|
|
1119
|
+
describe "#generate_many" do
|
|
1120
|
+
subject(:engine) { described_class.new(schema) }
|
|
1121
|
+
|
|
1122
|
+
it "generates multiple records" do
|
|
1123
|
+
records = engine.generate_many(100, seed: 42)
|
|
1124
|
+
|
|
1125
|
+
expect(records.size).to eq(100)
|
|
1126
|
+
expect(records).to all(be_a(Hash))
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
it "generates unique emails" do
|
|
1130
|
+
records = engine.generate_many(1000, seed: 42)
|
|
1131
|
+
emails = records.map { |r| r[:email] }
|
|
1132
|
+
|
|
1133
|
+
expect(emails.uniq.size).to eq(1000)
|
|
1134
|
+
end
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
describe "#generate_to_file" do
|
|
1138
|
+
subject(:engine) { described_class.new(schema) }
|
|
1139
|
+
let(:tempfile) { Tempfile.new(["test", ".jsonl"]) }
|
|
1140
|
+
|
|
1141
|
+
after { tempfile.unlink }
|
|
1142
|
+
|
|
1143
|
+
it "generates JSONL file" do
|
|
1144
|
+
count = engine.generate_to_file(100, tempfile.path, seed: 42, format: "jsonl")
|
|
1145
|
+
|
|
1146
|
+
expect(count).to eq(100)
|
|
1147
|
+
|
|
1148
|
+
lines = File.readlines(tempfile.path)
|
|
1149
|
+
expect(lines.size).to eq(100)
|
|
1150
|
+
|
|
1151
|
+
first_record = JSON.parse(lines.first)
|
|
1152
|
+
expect(first_record).to have_key("id")
|
|
1153
|
+
expect(first_record).to have_key("name")
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
it "generates CSV file" do
|
|
1157
|
+
count = engine.generate_to_file(100, tempfile.path, seed: 42, format: "csv")
|
|
1158
|
+
|
|
1159
|
+
expect(count).to eq(100)
|
|
1160
|
+
|
|
1161
|
+
lines = File.readlines(tempfile.path)
|
|
1162
|
+
expect(lines.first.strip).to eq("id,name,email,age,active")
|
|
1163
|
+
expect(lines.size).to eq(101) # header + 100 records
|
|
1164
|
+
end
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
describe "locale support" do
|
|
1168
|
+
it "generates Japanese names with ja_jp locale" do
|
|
1169
|
+
japanese_schema = FakeDataDSL.parse("User:\n name: name")
|
|
1170
|
+
engine = described_class.new(japanese_schema, locale: "ja_jp")
|
|
1171
|
+
|
|
1172
|
+
record = engine.generate(seed: 42)
|
|
1173
|
+
|
|
1174
|
+
# Japanese names should contain Japanese characters
|
|
1175
|
+
expect(record[:name]).to match(/[\p{Hiragana}\p{Katakana}\p{Han}]/)
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
it "generates German names with de_de locale" do
|
|
1179
|
+
german_schema = FakeDataDSL.parse("User:\n name: name")
|
|
1180
|
+
engine = described_class.new(german_schema, locale: "de_de")
|
|
1181
|
+
|
|
1182
|
+
record = engine.generate(seed: 42)
|
|
1183
|
+
|
|
1184
|
+
expect(record[:name]).to be_a(String)
|
|
1185
|
+
end
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
describe "performance", :benchmark do
|
|
1189
|
+
it "generates 1 million records quickly" do
|
|
1190
|
+
start_time = Time.now
|
|
1191
|
+
|
|
1192
|
+
engine = described_class.new(schema)
|
|
1193
|
+
tempfile = Tempfile.new(["benchmark", ".jsonl"])
|
|
1194
|
+
|
|
1195
|
+
engine.generate_to_file(1_000_000, tempfile.path, seed: 42)
|
|
1196
|
+
|
|
1197
|
+
elapsed = Time.now - start_time
|
|
1198
|
+
records_per_sec = 1_000_000 / elapsed
|
|
1199
|
+
|
|
1200
|
+
puts "\nNative engine: #{records_per_sec.round} records/sec"
|
|
1201
|
+
|
|
1202
|
+
expect(elapsed).to be < 10 # Should complete in under 10 seconds
|
|
1203
|
+
|
|
1204
|
+
tempfile.unlink
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
end
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
## 8. Packaging the Gem
|
|
1213
|
+
|
|
1214
|
+
### 8.1 Update `.gemspec`
|
|
1215
|
+
|
|
1216
|
+
```ruby
|
|
1217
|
+
Gem::Specification.new do |spec|
|
|
1218
|
+
spec.name = "fake_data_dsl"
|
|
1219
|
+
spec.version = FakeDataDSL::VERSION
|
|
1220
|
+
|
|
1221
|
+
# ... other fields ...
|
|
1222
|
+
|
|
1223
|
+
# Mark as having native extensions
|
|
1224
|
+
spec.extensions = ["ext/fake_data_dsl_native/extconf.rb"]
|
|
1225
|
+
|
|
1226
|
+
# Required Ruby version
|
|
1227
|
+
spec.required_ruby_version = ">= 3.0"
|
|
1228
|
+
|
|
1229
|
+
# Dependencies
|
|
1230
|
+
spec.add_dependency "rb_sys", "~> 0.9"
|
|
1231
|
+
|
|
1232
|
+
# Include all necessary files
|
|
1233
|
+
spec.files = Dir[
|
|
1234
|
+
"lib/**/*.rb",
|
|
1235
|
+
"ext/**/*.{rs,toml,rb}",
|
|
1236
|
+
"*.md",
|
|
1237
|
+
"LICENSE*"
|
|
1238
|
+
]
|
|
1239
|
+
end
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
### 8.2 Pre-built Binaries (Optional)
|
|
1243
|
+
|
|
1244
|
+
For faster installation, you can provide pre-built binaries:
|
|
1245
|
+
|
|
1246
|
+
```ruby
|
|
1247
|
+
# In Rakefile
|
|
1248
|
+
require "rb_sys/extensiontask"
|
|
1249
|
+
|
|
1250
|
+
RbSys::ExtensionTask.new("fake_data_dsl_native", GEMSPEC) do |ext|
|
|
1251
|
+
ext.lib_dir = "lib/fake_data_dsl"
|
|
1252
|
+
ext.cross_compile = true
|
|
1253
|
+
ext.cross_platform = %w[
|
|
1254
|
+
x86_64-linux
|
|
1255
|
+
x86_64-darwin
|
|
1256
|
+
arm64-darwin
|
|
1257
|
+
x86_64-linux-musl
|
|
1258
|
+
]
|
|
1259
|
+
end
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
---
|
|
1263
|
+
|
|
1264
|
+
## 9. Performance Benchmarks
|
|
1265
|
+
|
|
1266
|
+
### Expected Performance
|
|
1267
|
+
|
|
1268
|
+
| Operation | Ruby Engine | Native Engine | Speedup |
|
|
1269
|
+
|-----------|-------------|---------------|---------|
|
|
1270
|
+
| 1K records | 50ms | 2ms | 25x |
|
|
1271
|
+
| 10K records | 500ms | 5ms | 100x |
|
|
1272
|
+
| 100K records | 5s | 30ms | 166x |
|
|
1273
|
+
| 1M records | 50s | 300ms | 166x |
|
|
1274
|
+
| 10M records | 8min | 2s | 240x |
|
|
1275
|
+
|
|
1276
|
+
### Run Benchmarks
|
|
1277
|
+
|
|
1278
|
+
```ruby
|
|
1279
|
+
require "benchmark"
|
|
1280
|
+
require "fake_data_dsl"
|
|
1281
|
+
|
|
1282
|
+
schema = FakeDataDSL.parse(<<~DSL)
|
|
1283
|
+
User:
|
|
1284
|
+
id: uuid
|
|
1285
|
+
name: name
|
|
1286
|
+
email: email
|
|
1287
|
+
age: number(18..80)
|
|
1288
|
+
active: boolean
|
|
1289
|
+
DSL
|
|
1290
|
+
|
|
1291
|
+
Benchmark.bm(20) do |x|
|
|
1292
|
+
x.report("Ruby (1K):") { schema.generate_many(1_000) }
|
|
1293
|
+
|
|
1294
|
+
if FakeDataDSL::NativeEngine.available?
|
|
1295
|
+
engine = FakeDataDSL::NativeEngine.new(schema)
|
|
1296
|
+
x.report("Native (1K):") { engine.generate_many(1_000) }
|
|
1297
|
+
x.report("Native (100K):") { engine.generate_many(100_000) }
|
|
1298
|
+
x.report("Native (1M):") { engine.generate_many(1_000_000) }
|
|
1299
|
+
end
|
|
1300
|
+
end
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
## 10. Troubleshooting
|
|
1306
|
+
|
|
1307
|
+
### Common Issues
|
|
1308
|
+
|
|
1309
|
+
#### 1. "Rust not found"
|
|
1310
|
+
|
|
1311
|
+
```bash
|
|
1312
|
+
# Install Rust
|
|
1313
|
+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
1314
|
+
source ~/.cargo/env
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
#### 2. "rb_sys not found"
|
|
1318
|
+
|
|
1319
|
+
```bash
|
|
1320
|
+
gem install rb_sys
|
|
1321
|
+
bundle install
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
#### 3. Compilation errors
|
|
1325
|
+
|
|
1326
|
+
```bash
|
|
1327
|
+
# Clean and rebuild
|
|
1328
|
+
bundle exec rake clobber
|
|
1329
|
+
bundle exec rake compile
|
|
1330
|
+
|
|
1331
|
+
# Check Rust version
|
|
1332
|
+
rustc --version # Should be 1.70+
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
#### 4. "undefined symbol" on Linux
|
|
1336
|
+
|
|
1337
|
+
```bash
|
|
1338
|
+
# Install development headers
|
|
1339
|
+
sudo apt-get install ruby-dev libclang-dev
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
#### 5. macOS architecture issues
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
# For Apple Silicon
|
|
1346
|
+
rustup target add aarch64-apple-darwin
|
|
1347
|
+
|
|
1348
|
+
# For Intel
|
|
1349
|
+
rustup target add x86_64-apple-darwin
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1354
|
+
## Summary
|
|
1355
|
+
|
|
1356
|
+
With this integration:
|
|
1357
|
+
|
|
1358
|
+
1. **Install Rust** and create the extension structure
|
|
1359
|
+
2. **Write Rust code** using fake-rs for generation
|
|
1360
|
+
3. **Build** with `bundle exec rake compile`
|
|
1361
|
+
4. **Use** via `FakeDataDSL::NativeEngine`
|
|
1362
|
+
|
|
1363
|
+
```ruby
|
|
1364
|
+
# Check availability
|
|
1365
|
+
if FakeDataDSL::NativeEngine.available?
|
|
1366
|
+
engine = FakeDataDSL::NativeEngine.new(schema, locale: "en")
|
|
1367
|
+
|
|
1368
|
+
# Generate 10 million records in ~2 seconds
|
|
1369
|
+
engine.generate_to_file(10_000_000, "data.jsonl")
|
|
1370
|
+
end
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
---
|
|
1374
|
+
|
|
1375
|
+
*Last updated: January 2026*
|
|
1376
|
+
|