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,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
# High-performance native engine using Rust + fake-rs
|
|
5
|
+
#
|
|
6
|
+
# Provides 100-250x speedup over pure Ruby implementation by using
|
|
7
|
+
# a native Rust extension with the fake-rs library.
|
|
8
|
+
#
|
|
9
|
+
# @example Check availability
|
|
10
|
+
# if Synthra::NativeEngine.available?
|
|
11
|
+
# puts "Native engine is available!"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# schema = Synthra.parse("User:\n id: uuid\n name: name")
|
|
16
|
+
# engine = Synthra::NativeEngine.new(schema)
|
|
17
|
+
# records = engine.generate_many(1000)
|
|
18
|
+
#
|
|
19
|
+
# @example Generate to file
|
|
20
|
+
# engine.generate_to_file(10_000_000, "users.jsonl", format: "jsonl")
|
|
21
|
+
#
|
|
22
|
+
class NativeEngine
|
|
23
|
+
class << self
|
|
24
|
+
# Check if native extension is available
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] true if the native extension is loaded and functional
|
|
27
|
+
def available?
|
|
28
|
+
return @available if defined?(@available)
|
|
29
|
+
|
|
30
|
+
@available = begin
|
|
31
|
+
load_native_extension
|
|
32
|
+
defined?(Synthra::Native) &&
|
|
33
|
+
Native.respond_to?(:loaded?) &&
|
|
34
|
+
Native.loaded?
|
|
35
|
+
rescue LoadError, NameError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get list of types supported by native engine
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<String>] list of type names
|
|
43
|
+
def available_types
|
|
44
|
+
return [] unless available?
|
|
45
|
+
|
|
46
|
+
Native.available_types
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get list of locales supported by native engine
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<String>] list of locale codes
|
|
52
|
+
def available_locales
|
|
53
|
+
return [] unless available?
|
|
54
|
+
|
|
55
|
+
Native.available_locales
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get native extension version
|
|
59
|
+
#
|
|
60
|
+
# @return [String, nil] version string or nil if not available
|
|
61
|
+
def version
|
|
62
|
+
return nil unless available?
|
|
63
|
+
|
|
64
|
+
Native.version
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get performance information
|
|
68
|
+
#
|
|
69
|
+
# @return [String, nil] performance info or nil if not available
|
|
70
|
+
def performance_info
|
|
71
|
+
return nil unless available?
|
|
72
|
+
|
|
73
|
+
Native.performance_info
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the current number of threads used for parallel generation
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer, nil] thread count or nil if not available
|
|
79
|
+
def thread_count
|
|
80
|
+
return nil unless available?
|
|
81
|
+
|
|
82
|
+
Native.thread_count
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Set the number of threads to use for parallel generation
|
|
86
|
+
# Must be called before any generation operations.
|
|
87
|
+
#
|
|
88
|
+
# @param num_threads [Integer] number of threads (0 = use all cores)
|
|
89
|
+
# @return [Integer] actual number of threads configured
|
|
90
|
+
#
|
|
91
|
+
# @example Limit to 4 threads
|
|
92
|
+
# Synthra::NativeEngine.set_threads(4)
|
|
93
|
+
#
|
|
94
|
+
# @note Alternatively, set RAYON_NUM_THREADS env var before loading:
|
|
95
|
+
# RAYON_NUM_THREADS=4 ruby my_script.rb
|
|
96
|
+
def set_threads(num_threads)
|
|
97
|
+
return nil unless available?
|
|
98
|
+
|
|
99
|
+
Native.set_threads(num_threads)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def load_native_extension
|
|
105
|
+
require "synthra/synthra_native"
|
|
106
|
+
rescue LoadError => e
|
|
107
|
+
# Check if it's a missing extension or missing dependencies
|
|
108
|
+
if e.message.include?("cannot load such file")
|
|
109
|
+
raise LoadError, <<~MSG
|
|
110
|
+
Native extension not compiled. To enable native performance:
|
|
111
|
+
|
|
112
|
+
1. Install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
113
|
+
2. Install rb_sys: gem install rb_sys
|
|
114
|
+
3. Compile extension: bundle exec rake compile
|
|
115
|
+
|
|
116
|
+
Original error: #{e.message}
|
|
117
|
+
MSG
|
|
118
|
+
else
|
|
119
|
+
raise
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [Synthra::Schema] the schema being used
|
|
125
|
+
attr_reader :schema
|
|
126
|
+
|
|
127
|
+
# @return [String] the locale for data generation
|
|
128
|
+
attr_reader :locale
|
|
129
|
+
|
|
130
|
+
# Initialize native engine with a schema
|
|
131
|
+
#
|
|
132
|
+
# @param schema [Synthra::Schema] the schema to generate from
|
|
133
|
+
# @param locale [String] locale for fake data (en, fr_fr, de_de, ja_jp, etc.)
|
|
134
|
+
# @param threads [Integer, nil] number of threads (nil/0 = use all cores)
|
|
135
|
+
# @raise [LoadError] if native extension is not available
|
|
136
|
+
def initialize(schema, locale: "en", threads: nil)
|
|
137
|
+
unless self.class.available?
|
|
138
|
+
raise LoadError, "Native extension not available. " \
|
|
139
|
+
"Install Rust and run `bundle exec rake compile`"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
@schema = schema
|
|
143
|
+
@locale = locale.to_s
|
|
144
|
+
@threads = threads
|
|
145
|
+
@field_specs = build_field_specs
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Generate multiple records
|
|
149
|
+
#
|
|
150
|
+
# @param count [Integer] number of records to generate
|
|
151
|
+
# @param seed [Integer, nil] random seed for reproducibility
|
|
152
|
+
# @param threads [Integer, nil] number of threads (nil/0 = use all cores)
|
|
153
|
+
# @return [Array<Hash>] array of generated records
|
|
154
|
+
#
|
|
155
|
+
# @example Generate with default threads
|
|
156
|
+
# engine.generate_many(1000, seed: 42)
|
|
157
|
+
#
|
|
158
|
+
# @example Generate with limited threads
|
|
159
|
+
# engine.generate_many(1000, seed: 42, threads: 4)
|
|
160
|
+
def generate_many(count, seed: nil, threads: nil)
|
|
161
|
+
Native.generate(@field_specs, count, seed, @locale, threads || @threads)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Generate a single record
|
|
165
|
+
#
|
|
166
|
+
# @param seed [Integer, nil] random seed for reproducibility
|
|
167
|
+
# @return [Hash] generated record
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# record = engine.generate(seed: 42)
|
|
171
|
+
def generate(seed: nil)
|
|
172
|
+
Native.generate_one(@field_specs, seed, @locale)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Generate records directly to file (most efficient for large datasets)
|
|
176
|
+
#
|
|
177
|
+
# @param count [Integer] number of records to generate
|
|
178
|
+
# @param path [String] output file path
|
|
179
|
+
# @param seed [Integer, nil] random seed for reproducibility
|
|
180
|
+
# @param format [String] output format: "jsonl", "json", "csv"
|
|
181
|
+
# @param threads [Integer, nil] number of threads (nil/0 = use all cores)
|
|
182
|
+
# @return [Integer] number of records written
|
|
183
|
+
#
|
|
184
|
+
# @example Generate JSONL (one JSON per line)
|
|
185
|
+
# engine.generate_to_file(10_000_000, "users.jsonl", format: "jsonl")
|
|
186
|
+
#
|
|
187
|
+
# @example Generate CSV
|
|
188
|
+
# engine.generate_to_file(1_000_000, "users.csv", format: "csv")
|
|
189
|
+
#
|
|
190
|
+
# @example Generate with limited threads
|
|
191
|
+
# engine.generate_to_file(10_000_000, "users.jsonl", threads: 4)
|
|
192
|
+
def generate_to_file(count, path, seed: nil, format: "jsonl", threads: nil)
|
|
193
|
+
Native.generate_to_file(@field_specs, count, path.to_s, seed, @locale, format.to_s, threads || @threads)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Alias for compatibility with other engines
|
|
197
|
+
alias to_file generate_to_file
|
|
198
|
+
|
|
199
|
+
# Stream records (yields each record)
|
|
200
|
+
#
|
|
201
|
+
# @param count [Integer] number of records to generate
|
|
202
|
+
# @param seed [Integer, nil] random seed for reproducibility
|
|
203
|
+
# @yield [Hash] each generated record
|
|
204
|
+
# @return [Enumerator] if no block given
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# engine.stream(1000) do |record|
|
|
208
|
+
# process(record)
|
|
209
|
+
# end
|
|
210
|
+
def stream(count, seed: nil, &block)
|
|
211
|
+
return enum_for(:stream, count, seed: seed) unless block_given?
|
|
212
|
+
|
|
213
|
+
# For now, generate in batches of 10K for memory efficiency
|
|
214
|
+
batch_size = 10_000
|
|
215
|
+
remaining = count
|
|
216
|
+
offset = 0
|
|
217
|
+
|
|
218
|
+
while remaining > 0
|
|
219
|
+
current_batch = [remaining, batch_size].min
|
|
220
|
+
batch_seed = seed ? seed + offset : nil
|
|
221
|
+
|
|
222
|
+
records = Native.generate(@field_specs, current_batch, batch_seed, @locale, @threads)
|
|
223
|
+
records.each(&block)
|
|
224
|
+
|
|
225
|
+
remaining -= current_batch
|
|
226
|
+
offset += current_batch
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Build field specs for native engine
|
|
233
|
+
# Format: [name, type_name, args_json, optional, nullable]
|
|
234
|
+
def build_field_specs
|
|
235
|
+
@schema.fields.map do |field|
|
|
236
|
+
[
|
|
237
|
+
field.name.to_s,
|
|
238
|
+
map_type_name(field.type_name),
|
|
239
|
+
build_args_json(field),
|
|
240
|
+
field.optional?,
|
|
241
|
+
field.nullable?
|
|
242
|
+
]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Map DSL type names to native engine type names
|
|
247
|
+
def map_type_name(type_name)
|
|
248
|
+
type_str = type_name.to_s
|
|
249
|
+
|
|
250
|
+
# Handle type aliases
|
|
251
|
+
case type_str
|
|
252
|
+
when "full_name" then "name"
|
|
253
|
+
when "phone_number" then "phone"
|
|
254
|
+
when "street_address" then "street"
|
|
255
|
+
when "zip_code" then "postal_code"
|
|
256
|
+
when "int", "integer" then "number"
|
|
257
|
+
when "bool" then "boolean"
|
|
258
|
+
when "timestamp" then "datetime"
|
|
259
|
+
else type_str
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Build args JSON for a field
|
|
264
|
+
def build_args_json(field)
|
|
265
|
+
args = field.type_args || {}
|
|
266
|
+
|
|
267
|
+
# Handle enum values
|
|
268
|
+
if field.type_name.to_s == "enum" && args[:values]
|
|
269
|
+
args = args.merge(values: args[:values].map { |v| extract_enum_value(v) })
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Handle const value
|
|
273
|
+
if field.type_name.to_s == "const"
|
|
274
|
+
args = { value: args[:value] || args.values.first }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Handle number ranges
|
|
278
|
+
if args[:range]
|
|
279
|
+
range = args[:range]
|
|
280
|
+
args = args.merge(min: range.begin, max: range.end)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
args.to_json
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Extract the string value from an enum value (handles AST nodes and hashes)
|
|
287
|
+
def extract_enum_value(value)
|
|
288
|
+
case value
|
|
289
|
+
when Hash
|
|
290
|
+
value[:value] || value["value"]
|
|
291
|
+
when String, Symbol
|
|
292
|
+
value.to_s
|
|
293
|
+
else
|
|
294
|
+
# Handle AST nodes like EnumValueNode
|
|
295
|
+
if value.respond_to?(:value)
|
|
296
|
+
value.value.to_s
|
|
297
|
+
else
|
|
298
|
+
value.to_s
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Synthra
|
|
7
|
+
# OpenAPI/Swagger Schema Importer
|
|
8
|
+
#
|
|
9
|
+
# Converts OpenAPI specifications to Synthra schemas.
|
|
10
|
+
#
|
|
11
|
+
# @example CLI usage
|
|
12
|
+
# $ synthra import openapi.yaml --output schemas/
|
|
13
|
+
#
|
|
14
|
+
# @example Ruby usage
|
|
15
|
+
# importer = Synthra::OpenAPIImporter.new("openapi.yaml")
|
|
16
|
+
# schemas = importer.import
|
|
17
|
+
# schemas.each { |name, dsl| File.write("schemas/#{name}.dsl", dsl) }
|
|
18
|
+
#
|
|
19
|
+
class OpenAPIImporter
|
|
20
|
+
# OpenAPI type to Synthra type mapping
|
|
21
|
+
TYPE_MAP = {
|
|
22
|
+
# Strings
|
|
23
|
+
"string" => "text",
|
|
24
|
+
"string:email" => "email",
|
|
25
|
+
"string:uri" => "url",
|
|
26
|
+
"string:url" => "url",
|
|
27
|
+
"string:uuid" => "uuid",
|
|
28
|
+
"string:date" => "date",
|
|
29
|
+
"string:date-time" => "timestamp",
|
|
30
|
+
"string:password" => "password",
|
|
31
|
+
"string:byte" => "text",
|
|
32
|
+
"string:binary" => "text",
|
|
33
|
+
"string:hostname" => "domain",
|
|
34
|
+
"string:ipv4" => "ip",
|
|
35
|
+
"string:ipv6" => "ipv6",
|
|
36
|
+
|
|
37
|
+
# Numbers
|
|
38
|
+
"integer" => "number",
|
|
39
|
+
"integer:int32" => "number",
|
|
40
|
+
"integer:int64" => "number",
|
|
41
|
+
"number" => "float",
|
|
42
|
+
"number:float" => "float",
|
|
43
|
+
"number:double" => "float",
|
|
44
|
+
|
|
45
|
+
# Boolean
|
|
46
|
+
"boolean" => "boolean",
|
|
47
|
+
|
|
48
|
+
# Complex
|
|
49
|
+
"array" => "array",
|
|
50
|
+
"object" => "object"
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
attr_reader :spec_path, :spec
|
|
54
|
+
|
|
55
|
+
# Create a new importer
|
|
56
|
+
#
|
|
57
|
+
# @param spec_path [String] path to OpenAPI spec file
|
|
58
|
+
#
|
|
59
|
+
def initialize(spec_path)
|
|
60
|
+
@spec_path = spec_path
|
|
61
|
+
@spec = load_spec
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Import all schemas from the OpenAPI spec
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash<String, String>] schema name => DSL content
|
|
67
|
+
#
|
|
68
|
+
def import
|
|
69
|
+
schemas = {}
|
|
70
|
+
|
|
71
|
+
components_schemas.each do |name, definition|
|
|
72
|
+
dsl = schema_to_dsl(name, definition)
|
|
73
|
+
schemas[name] = dsl if dsl
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
schemas
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Import and write to directory
|
|
80
|
+
#
|
|
81
|
+
# @param output_dir [String] output directory
|
|
82
|
+
# @return [Array<String>] paths to created files
|
|
83
|
+
#
|
|
84
|
+
def import_to_directory(output_dir)
|
|
85
|
+
FileUtils.mkdir_p(output_dir)
|
|
86
|
+
created = []
|
|
87
|
+
|
|
88
|
+
base = File.expand_path(output_dir)
|
|
89
|
+
import.each do |name, dsl|
|
|
90
|
+
# SECURITY (path traversal): a schema name from the spec is attacker-controlled. basename
|
|
91
|
+
# strips any "../" path components and the allowlist drops anything left, so the file can only
|
|
92
|
+
# land inside output_dir; the expand_path assertion is a belt-and-suspenders backstop.
|
|
93
|
+
safe = File.basename(to_snake_case(name.to_s)).gsub(/[^a-z0-9_]+/i, "_")
|
|
94
|
+
# :nocov: defensive backstop — File.basename + the gsub allowlist always leave a non-empty
|
|
95
|
+
# token for any non-empty schema name, so this empty-fallback is unreachable in practice.
|
|
96
|
+
safe = "schema" if safe.empty?
|
|
97
|
+
# :nocov:
|
|
98
|
+
path = File.join(output_dir, "#{safe}.dsl")
|
|
99
|
+
# :nocov: defensive backstop — basename already neutralizes "../" traversal, so this
|
|
100
|
+
# expand_path containment check can never fail for a name that produced a safe basename.
|
|
101
|
+
unless File.expand_path(path).start_with?(base + File::SEPARATOR)
|
|
102
|
+
raise Synthra::Errors::SecurityError, "Refusing to write outside #{output_dir}: #{name.inspect}"
|
|
103
|
+
end
|
|
104
|
+
# :nocov:
|
|
105
|
+
|
|
106
|
+
File.write(path, dsl)
|
|
107
|
+
created << path
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
created
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def load_spec
|
|
116
|
+
content = File.read(@spec_path)
|
|
117
|
+
|
|
118
|
+
if @spec_path.end_with?(".json")
|
|
119
|
+
JSON.parse(content)
|
|
120
|
+
else
|
|
121
|
+
YAML.safe_load(content, permitted_classes: [Date, Time])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def components_schemas
|
|
126
|
+
@spec.dig("components", "schemas") ||
|
|
127
|
+
@spec.dig("definitions") || # Swagger 2.0
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def schema_to_dsl(name, definition)
|
|
132
|
+
return nil unless definition["type"] == "object" || definition["properties"]
|
|
133
|
+
|
|
134
|
+
lines = ["#{name}:"]
|
|
135
|
+
properties = definition["properties"] || {}
|
|
136
|
+
required = definition["required"] || []
|
|
137
|
+
|
|
138
|
+
properties.each do |prop_name, prop_def|
|
|
139
|
+
type = property_to_type(prop_def)
|
|
140
|
+
optional = required.include?(prop_name) ? "" : "?"
|
|
141
|
+
nullable = prop_def["nullable"] ? "?" : ""
|
|
142
|
+
|
|
143
|
+
lines << " #{prop_name}#{optional}: #{type}#{nullable}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
lines.join("\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def property_to_type(prop_def)
|
|
150
|
+
type = prop_def["type"]
|
|
151
|
+
format = prop_def["format"]
|
|
152
|
+
|
|
153
|
+
# Handle references
|
|
154
|
+
if prop_def["$ref"]
|
|
155
|
+
ref = prop_def["$ref"]
|
|
156
|
+
return extract_ref_name(ref)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Handle enums
|
|
160
|
+
if prop_def["enum"]
|
|
161
|
+
values = prop_def["enum"].join(", ")
|
|
162
|
+
return "enum(#{values})"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Handle arrays
|
|
166
|
+
if type == "array"
|
|
167
|
+
items_type = property_to_type(prop_def["items"] || {})
|
|
168
|
+
min_items = prop_def["minItems"] || 0
|
|
169
|
+
max_items = prop_def["maxItems"] || 10
|
|
170
|
+
return "array(#{items_type}, #{min_items}..#{max_items})"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Handle nested objects
|
|
174
|
+
if type == "object"
|
|
175
|
+
if prop_def["properties"]
|
|
176
|
+
# Inline object - use object type
|
|
177
|
+
return "object"
|
|
178
|
+
end
|
|
179
|
+
return "object"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check for string constraints
|
|
183
|
+
if type == "string"
|
|
184
|
+
# Check for specific patterns
|
|
185
|
+
if (pattern = prop_def["pattern"])
|
|
186
|
+
return "regex(\"#{escape_regex(pattern)}\")"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check for length constraints
|
|
190
|
+
min_len = prop_def["minLength"]
|
|
191
|
+
max_len = prop_def["maxLength"]
|
|
192
|
+
if min_len || max_len
|
|
193
|
+
range = "#{min_len || 1}..#{max_len || 100}"
|
|
194
|
+
return "text(#{range})"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Check for number constraints
|
|
199
|
+
if %w[integer number].include?(type)
|
|
200
|
+
min_val = prop_def["minimum"]
|
|
201
|
+
max_val = prop_def["maximum"]
|
|
202
|
+
if min_val || max_val
|
|
203
|
+
range = "#{min_val || 0}..#{max_val || 1000}"
|
|
204
|
+
return "number(#{range})"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Look up in type map
|
|
209
|
+
key = format ? "#{type}:#{format}" : type
|
|
210
|
+
TYPE_MAP[key] || TYPE_MAP[type] || "text"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def extract_ref_name(ref)
|
|
214
|
+
# Handle #/components/schemas/User or #/definitions/User
|
|
215
|
+
ref.split("/").last
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def escape_regex(pattern)
|
|
219
|
+
pattern.gsub('"', '\\"')
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_snake_case(str)
|
|
223
|
+
str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
224
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
225
|
+
.downcase
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Output
|
|
5
|
+
|
|
6
|
+
# JSON formatter for generated data
|
|
7
|
+
class JsonFormatter
|
|
8
|
+
|
|
9
|
+
# Format a single record
|
|
10
|
+
#
|
|
11
|
+
# @param record [Hash] the record
|
|
12
|
+
# @param pretty [Boolean] pretty-print
|
|
13
|
+
# @return [String] JSON string
|
|
14
|
+
|
|
15
|
+
def format(record, pretty: false)
|
|
16
|
+
if pretty
|
|
17
|
+
JSON.pretty_generate(record)
|
|
18
|
+
else
|
|
19
|
+
JSON.generate(record)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Format multiple records
|
|
25
|
+
#
|
|
26
|
+
# @param records [Array<Hash>] the records
|
|
27
|
+
# @param pretty [Boolean] pretty-print
|
|
28
|
+
# @return [String] JSON string
|
|
29
|
+
|
|
30
|
+
def format_many(records, pretty: false)
|
|
31
|
+
if pretty
|
|
32
|
+
JSON.pretty_generate(records)
|
|
33
|
+
else
|
|
34
|
+
JSON.generate(records)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Output
|
|
5
|
+
|
|
6
|
+
# NDJSON (newline-delimited JSON) formatter
|
|
7
|
+
class NdjsonFormatter
|
|
8
|
+
|
|
9
|
+
# Format a stream to NDJSON
|
|
10
|
+
#
|
|
11
|
+
# @param stream [Enumerator] the record stream
|
|
12
|
+
# @param io [IO] output IO object
|
|
13
|
+
# @return [void]
|
|
14
|
+
|
|
15
|
+
def format_stream(stream, io: $stdout)
|
|
16
|
+
stream.each do |record|
|
|
17
|
+
io.puts JSON.generate(record)
|
|
18
|
+
io.flush if io.respond_to?(:flush)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Format records to NDJSON string
|
|
24
|
+
#
|
|
25
|
+
# @param records [Array<Hash>] the records
|
|
26
|
+
# @return [String] NDJSON string
|
|
27
|
+
|
|
28
|
+
def format_many(records)
|
|
29
|
+
# Append into one buffer instead of allocating an N-element intermediate array of strings.
|
|
30
|
+
# Identical output to map.join("\n") (no trailing newline). For true O(1) memory, stream
|
|
31
|
+
# to an IO with #format_stream instead of materialising the whole string.
|
|
32
|
+
buffer = +""
|
|
33
|
+
records.each_with_index do |record, i|
|
|
34
|
+
buffer << "\n" if i.positive?
|
|
35
|
+
buffer << JSON.generate(record)
|
|
36
|
+
end
|
|
37
|
+
buffer
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Write to file
|
|
42
|
+
#
|
|
43
|
+
# @param stream [Enumerator] the record stream
|
|
44
|
+
# @param path [String] file path
|
|
45
|
+
# @return [void]
|
|
46
|
+
|
|
47
|
+
def write_to_file(stream, path)
|
|
48
|
+
File.open(path, "w") do |file|
|
|
49
|
+
format_stream(stream, io: file)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|