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.
Files changed (302) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +40 -0
  4. data/.yardopts +18 -0
  5. data/ACCESS_DOCS.md +116 -0
  6. data/ADVANCED_FEATURES_SUMMARY.md +245 -0
  7. data/CHANGELOG.md +498 -0
  8. data/CODE_OF_CONDUCT.md +122 -0
  9. data/CONTRIBUTING.md +307 -0
  10. data/DOCUMENTATION.md +204 -0
  11. data/DOCUMENTATION_GUIDE.md +194 -0
  12. data/LICENSE +22 -0
  13. data/README.md +1458 -0
  14. data/Rakefile +128 -0
  15. data/benchmark/README.md +591 -0
  16. data/benchmark/batch_generation.rb +63 -0
  17. data/benchmark/engine_comparison.rb +295 -0
  18. data/benchmark/single_record.rb +108 -0
  19. data/benchmark/streaming.rb +74 -0
  20. data/docs/ACTIVERECORD_INFERENCE.md +524 -0
  21. data/docs/API_SERVER.md +278 -0
  22. data/docs/CONFIG_FILE.md +315 -0
  23. data/docs/DATA_CONTRACTS.md +312 -0
  24. data/docs/ENHANCED_REPL.md +304 -0
  25. data/docs/FACTORY_BOT.md +271 -0
  26. data/docs/GITHUB_ACTION.md +420 -0
  27. data/docs/GRAPHQL_EXPORT.md +303 -0
  28. data/docs/GRAPHQL_FEDERATION.md +651 -0
  29. data/docs/HOW_TO_GENERATE_DOCS.md +252 -0
  30. data/docs/LSP.md +189 -0
  31. data/docs/MIGRATION_GENERATOR.md +547 -0
  32. data/docs/MOCK_SERVER.md +280 -0
  33. data/docs/NATIVE_ENGINE.md +298 -0
  34. data/docs/OPENAPI_EXPORT.md +252 -0
  35. data/docs/PERFORMANCE_MODE.md +228 -0
  36. data/docs/PERSONAS.md +547 -0
  37. data/docs/PROPERTY_TESTING.md +284 -0
  38. data/docs/PROTOBUF_EXPORT.md +276 -0
  39. data/docs/QUALITY_METRICS.md +555 -0
  40. data/docs/QUICK_REFERENCE.md +88 -0
  41. data/docs/RAILS_ENGINE.md +404 -0
  42. data/docs/RAILS_INTEGRATION.md +454 -0
  43. data/docs/README.md +175 -0
  44. data/docs/REPL_QUICK_REFERENCE.md +149 -0
  45. data/docs/RUST_INTEGRATION_GUIDE.md +1376 -0
  46. data/docs/SCENARIOS.md +576 -0
  47. data/docs/SECURITY_TESTING.md +318 -0
  48. data/docs/SNAPSHOT_TESTING.md +308 -0
  49. data/docs/STRING_TEXT_TYPES_REFERENCE.md +512 -0
  50. data/docs/TERRAFORM_EXPORT.md +447 -0
  51. data/docs/TIME_TRAVEL.md +507 -0
  52. data/docs/USAGE_GUIDE.md +980 -0
  53. data/docs/WEBHOOK_SIMULATOR.md +579 -0
  54. data/docs/advanced_features.md +183 -0
  55. data/docs/api_reference.md +153 -0
  56. data/docs/behaviors.md +138 -0
  57. data/docs/best_practices.md +191 -0
  58. data/docs/dsl_reference.md +172 -0
  59. data/docs/examples.md +162 -0
  60. data/docs/generation_modes.md +125 -0
  61. data/docs/getting_started.md +92 -0
  62. data/docs/index.html +177 -0
  63. data/docs/strategic/CLOUD_REGISTRY_ARCHITECTURE.md +268 -0
  64. data/docs/strategic/SIMD_OPTIMIZATION_STRATEGY.md +153 -0
  65. data/docs/troubleshooting.md +215 -0
  66. data/docs/type_reference.md +224 -0
  67. data/examples/hostile_mode_demo.rb +64 -0
  68. data/exe/synthra +6 -0
  69. data/ext/synthra_native/Cargo.lock +972 -0
  70. data/ext/synthra_native/Cargo.toml +46 -0
  71. data/ext/synthra_native/extconf.rb +41 -0
  72. data/ext/synthra_native/src/generator.rs +397 -0
  73. data/ext/synthra_native/src/lib.rs +229 -0
  74. data/ext/synthra_native/src/types.rs +1485 -0
  75. data/lib/generators/synthra/install_generator.rb +86 -0
  76. data/lib/generators/synthra/templates/api_response.dsl +15 -0
  77. data/lib/generators/synthra/templates/fake_data.rake +116 -0
  78. data/lib/generators/synthra/templates/synthra.yml +31 -0
  79. data/lib/generators/synthra/templates/synthra_support.rb +19 -0
  80. data/lib/generators/synthra/templates/user.dsl +11 -0
  81. data/lib/synthra/activerecord_inference.rb +394 -0
  82. data/lib/synthra/api.rb +381 -0
  83. data/lib/synthra/api_server.rb +718 -0
  84. data/lib/synthra/behaviors/applicator.rb +165 -0
  85. data/lib/synthra/behaviors/base.rb +155 -0
  86. data/lib/synthra/behaviors/close_connection.rb +60 -0
  87. data/lib/synthra/behaviors/deprecated.rb +100 -0
  88. data/lib/synthra/behaviors/failure.rb +69 -0
  89. data/lib/synthra/behaviors/latency.rb +103 -0
  90. data/lib/synthra/behaviors/partial_data.rb +83 -0
  91. data/lib/synthra/behaviors/randomize_order.rb +74 -0
  92. data/lib/synthra/behaviors/registry.rb +238 -0
  93. data/lib/synthra/behaviors/simulate_error.rb +79 -0
  94. data/lib/synthra/cli/commands/base.rb +105 -0
  95. data/lib/synthra/cli/commands/diff.rb +227 -0
  96. data/lib/synthra/cli/commands/docs.rb +67 -0
  97. data/lib/synthra/cli/commands/export.rb +150 -0
  98. data/lib/synthra/cli/commands/generate.rb +72 -0
  99. data/lib/synthra/cli/commands/import.rb +80 -0
  100. data/lib/synthra/cli/commands/lint.rb +53 -0
  101. data/lib/synthra/cli/commands/live.rb +57 -0
  102. data/lib/synthra/cli/commands/seed.rb +99 -0
  103. data/lib/synthra/cli/commands/validate.rb +28 -0
  104. data/lib/synthra/cli.rb +2471 -0
  105. data/lib/synthra/config_file.rb +129 -0
  106. data/lib/synthra/configuration.rb +281 -0
  107. data/lib/synthra/contracts_registry.rb +408 -0
  108. data/lib/synthra/database_seeder.rb +268 -0
  109. data/lib/synthra/deterministic_ids.rb +218 -0
  110. data/lib/synthra/documentation_generator.rb +414 -0
  111. data/lib/synthra/engine.rb +251 -0
  112. data/lib/synthra/errors.rb +1169 -0
  113. data/lib/synthra/export/base.rb +85 -0
  114. data/lib/synthra/export/csv.rb +101 -0
  115. data/lib/synthra/export/graphql.rb +266 -0
  116. data/lib/synthra/export/graphql_federation.rb +377 -0
  117. data/lib/synthra/export/graphviz.rb +258 -0
  118. data/lib/synthra/export/javascript.rb +327 -0
  119. data/lib/synthra/export/json_data.rb +61 -0
  120. data/lib/synthra/export/json_schema.rb +290 -0
  121. data/lib/synthra/export/openapi.rb +514 -0
  122. data/lib/synthra/export/protobuf.rb +483 -0
  123. data/lib/synthra/export/python.rb +560 -0
  124. data/lib/synthra/export/sql.rb +381 -0
  125. data/lib/synthra/export/sql_insert.rb +152 -0
  126. data/lib/synthra/export/terraform.rb +501 -0
  127. data/lib/synthra/export/type_mapping.rb +389 -0
  128. data/lib/synthra/export/typescript.rb +300 -0
  129. data/lib/synthra/export/xml_data.rb +104 -0
  130. data/lib/synthra/export/yaml_data.rb +56 -0
  131. data/lib/synthra/export.rb +404 -0
  132. data/lib/synthra/factory_bot_integration.rb +157 -0
  133. data/lib/synthra/field.rb +440 -0
  134. data/lib/synthra/functions/registry.rb +104 -0
  135. data/lib/synthra/generator/context.rb +372 -0
  136. data/lib/synthra/generator/engine.rb +336 -0
  137. data/lib/synthra/generator/faker_adapter.rb +425 -0
  138. data/lib/synthra/generator/modes.rb +444 -0
  139. data/lib/synthra/generator/resolver.rb +256 -0
  140. data/lib/synthra/generator/rng.rb +273 -0
  141. data/lib/synthra/generator/streamer.rb +63 -0
  142. data/lib/synthra/generator/uniqueness.rb +118 -0
  143. data/lib/synthra/initializer.rb +97 -0
  144. data/lib/synthra/limits.rb +279 -0
  145. data/lib/synthra/live_preview.rb +518 -0
  146. data/lib/synthra/loader_config.rb +78 -0
  147. data/lib/synthra/lsp/server.rb +689 -0
  148. data/lib/synthra/migration_generator.rb +301 -0
  149. data/lib/synthra/mixin.rb +174 -0
  150. data/lib/synthra/mock_server.rb +458 -0
  151. data/lib/synthra/native_engine.rb +304 -0
  152. data/lib/synthra/openapi_importer.rb +228 -0
  153. data/lib/synthra/output/json_formatter.rb +40 -0
  154. data/lib/synthra/output/ndjson_formatter.rb +55 -0
  155. data/lib/synthra/parser/ast.rb +1287 -0
  156. data/lib/synthra/parser/lexer.rb +1152 -0
  157. data/lib/synthra/parser/parser.rb +1664 -0
  158. data/lib/synthra/parser/tokens.rb +461 -0
  159. data/lib/synthra/performance_mode.rb +364 -0
  160. data/lib/synthra/personas.rb +397 -0
  161. data/lib/synthra/property_testing.rb +172 -0
  162. data/lib/synthra/quality_metrics.rb +405 -0
  163. data/lib/synthra/rails_test_helper.rb +248 -0
  164. data/lib/synthra/registry.rb +533 -0
  165. data/lib/synthra/relationships.rb +193 -0
  166. data/lib/synthra/repl/enhanced_repl.rb +607 -0
  167. data/lib/synthra/repl/formatter.rb +191 -0
  168. data/lib/synthra/scenarios.rb +423 -0
  169. data/lib/synthra/schema.rb +605 -0
  170. data/lib/synthra/schema_inheritance.rb +104 -0
  171. data/lib/synthra/schema_versioning.rb +212 -0
  172. data/lib/synthra/snapshot_testing.rb +199 -0
  173. data/lib/synthra/time_travel.rb +338 -0
  174. data/lib/synthra/type_definitions.rb +274 -0
  175. data/lib/synthra/types/address_location/addresses.rb +125 -0
  176. data/lib/synthra/types/address_location/airports.rb +200 -0
  177. data/lib/synthra/types/address_location/banks_hospitals.rb +207 -0
  178. data/lib/synthra/types/address_location/locations.rb +406 -0
  179. data/lib/synthra/types/base.rb +48 -0
  180. data/lib/synthra/types/commerce_products/commerce.rb +119 -0
  181. data/lib/synthra/types/commerce_products/companies.rb +71 -0
  182. data/lib/synthra/types/commerce_products/construction.rb +126 -0
  183. data/lib/synthra/types/commerce_products/products.rb +205 -0
  184. data/lib/synthra/types/core/collections.rb +513 -0
  185. data/lib/synthra/types/core/defaults.rb +76 -0
  186. data/lib/synthra/types/core/enums.rb +102 -0
  187. data/lib/synthra/types/core/identifiers.rb +445 -0
  188. data/lib/synthra/types/core/primitives.rb +586 -0
  189. data/lib/synthra/types/core/references.rb +466 -0
  190. data/lib/synthra/types/core/sequences.rb +304 -0
  191. data/lib/synthra/types/crypto/crypto.rb +162 -0
  192. data/lib/synthra/types/date_time/dates.rb +387 -0
  193. data/lib/synthra/types/finance_banking/banking.rb +424 -0
  194. data/lib/synthra/types/finance_banking/credit_cards.rb +24 -0
  195. data/lib/synthra/types/finance_banking/identifiers.rb +93 -0
  196. data/lib/synthra/types/formula.rb +184 -0
  197. data/lib/synthra/types/health_medical/medical.rb +150 -0
  198. data/lib/synthra/types/hostile_payloads.rb +122 -0
  199. data/lib/synthra/types/json_array.rb +51 -0
  200. data/lib/synthra/types/media_entertainment/media.rb +62 -0
  201. data/lib/synthra/types/naughty_string.rb +30 -0
  202. data/lib/synthra/types/personal_names/chinese.rb +41 -0
  203. data/lib/synthra/types/personal_names/identifiers.rb +187 -0
  204. data/lib/synthra/types/personal_names/names.rb +246 -0
  205. data/lib/synthra/types/personal_names/national_id.rb +89 -0
  206. data/lib/synthra/types/personal_names/titles_suffixes.rb +41 -0
  207. data/lib/synthra/types/regex.rb +248 -0
  208. data/lib/synthra/types/registry.rb +182 -0
  209. data/lib/synthra/types/repeating_element.rb +50 -0
  210. data/lib/synthra/types/scenario.rb +29 -0
  211. data/lib/synthra/types/technology_internet/apps.rb +67 -0
  212. data/lib/synthra/types/technology_internet/communication.rb +162 -0
  213. data/lib/synthra/types/technology_internet/devices.rb +80 -0
  214. data/lib/synthra/types/technology_internet/formats.rb +139 -0
  215. data/lib/synthra/types/technology_internet/networking.rb +143 -0
  216. data/lib/synthra/types/template.rb +92 -0
  217. data/lib/synthra/types/text_content/business.rb +128 -0
  218. data/lib/synthra/types/text_content/colors.rb +70 -0
  219. data/lib/synthra/types/text_content/misc.rb +237 -0
  220. data/lib/synthra/types/text_content/security.rb +130 -0
  221. data/lib/synthra/types/text_content/text_generation.rb +532 -0
  222. data/lib/synthra/types/travel/travel.rb +150 -0
  223. data/lib/synthra/utils/string_distance.rb +133 -0
  224. data/lib/synthra/validator/dsl_validator.rb +339 -0
  225. data/lib/synthra/validator/path_validator.rb +615 -0
  226. data/lib/synthra/version.rb +35 -0
  227. data/lib/synthra/webhook_simulator.rb +341 -0
  228. data/lib/synthra.rb +259 -0
  229. data/schemas/address.dsl +16 -0
  230. data/schemas/api_response.dsl +8 -0
  231. data/schemas/error_payload.dsl +9 -0
  232. data/schemas/order.dsl +10 -0
  233. data/schemas/order_item.dsl +8 -0
  234. data/schemas/payment.dsl +11 -0
  235. data/schemas/social_post.dsl +13 -0
  236. data/schemas/user.dsl +10 -0
  237. data/scripts/batch_fix_all.rb +96 -0
  238. data/scripts/delete_old_files.rb +32 -0
  239. data/scripts/fix_all_grouped_files.rb +137 -0
  240. data/scripts/fix_all_indentation.rb +48 -0
  241. data/scripts/fix_all_syntax.rb +124 -0
  242. data/scripts/fix_grouped_files.rb +184 -0
  243. data/scripts/fix_syntax_errors.rb +108 -0
  244. data/scripts/group_domain_types.rb +112 -0
  245. data/scripts/group_domain_types_fixed.rb +150 -0
  246. data/scripts/merge_domains_to_one_file.rb +68 -0
  247. data/scripts/move_existing_types.rb +130 -0
  248. data/scripts/split_grouped_types.rb +142 -0
  249. data/tech_docs/README.md +134 -0
  250. data/tech_docs/advanced/streaming.md +405 -0
  251. data/tech_docs/advanced/thread_safety.md +253 -0
  252. data/tech_docs/api/overview.md +485 -0
  253. data/tech_docs/appendices/type_chart.md +193 -0
  254. data/tech_docs/basic_concepts.md +383 -0
  255. data/tech_docs/behaviors/overview.md +346 -0
  256. data/tech_docs/dsl/complex_types.md +790 -0
  257. data/tech_docs/dsl/core_types.md +464 -0
  258. data/tech_docs/dsl/datetime_types.md +325 -0
  259. data/tech_docs/dsl/field_modifiers.md +414 -0
  260. data/tech_docs/dsl/grammar.md +431 -0
  261. data/tech_docs/dsl/schema_definition.md +399 -0
  262. data/tech_docs/export/README.md +276 -0
  263. data/tech_docs/installation.md +273 -0
  264. data/tech_docs/integration/ci_cd.md +707 -0
  265. data/tech_docs/integration/ci_cd_guide.md +579 -0
  266. data/tech_docs/integration/factory_bot.md +485 -0
  267. data/tech_docs/integration/rails.md +630 -0
  268. data/tech_docs/integration/rspec.md +449 -0
  269. data/tech_docs/modes/overview.md +350 -0
  270. data/tech_docs/performance/NATIVE_RUST_EXTENSION.md +1270 -0
  271. data/tech_docs/performance/OPTIMIZATION_GUIDE.md +901 -0
  272. data/tech_docs/quick_start.md +256 -0
  273. data/tech_docs/templates/README.md +805 -0
  274. data/tech_docs/tutorials/advanced.md +371 -0
  275. data/tech_docs/tutorials/getting_started.md +189 -0
  276. data/tech_docs/tutorials/intermediate.md +231 -0
  277. data/tech_docs/tutorials/template_gallery.md +569 -0
  278. data/tech_example/01_basic_usage.rb +238 -0
  279. data/tech_example/02_types_demo.rb +336 -0
  280. data/tech_example/04_cli_usage.md +429 -0
  281. data/tech_example/05_database_seeding.rb +283 -0
  282. data/tech_example/07_rspec_integration.rb +359 -0
  283. data/tech_example/10_custom_types.rb +387 -0
  284. data/tech_example/12_behaviors.rb +372 -0
  285. data/tech_example/13_twitter_dm_example.rb +120 -0
  286. data/tech_example/14_exact_json_structure.rb +253 -0
  287. data/tech_example/16_lsp_server.rb +56 -0
  288. data/tech_example/17_property_testing.rb +92 -0
  289. data/tech_example/18_enhanced_repl.rb +120 -0
  290. data/tech_example/19_security_fuzzing.rb +145 -0
  291. data/tech_example/NEW_FEATURES.md +169 -0
  292. data/tech_example/README.md +118 -0
  293. data/tech_example/schemas/api_response.dsl +123 -0
  294. data/tech_example/schemas/ecommerce.dsl +121 -0
  295. data/tech_example/schemas/twitter_dm.dsl +109 -0
  296. data/tech_example/schemas/user.dsl +36 -0
  297. data/vscode-extension/README.md +246 -0
  298. data/vscode-extension/language-configuration.json +31 -0
  299. data/vscode-extension/package.json +55 -0
  300. data/vscode-extension/snippets/fakedatadsl.json +198 -0
  301. data/vscode-extension/syntaxes/fakedatadsl.tmLanguage.json +128 -0
  302. 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
+