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,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :nocov:
|
|
4
|
+
module Synthra
|
|
5
|
+
module Export
|
|
6
|
+
# Base class for all exporters
|
|
7
|
+
#
|
|
8
|
+
# Provides common functionality for exporting schemas to different formats.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclass and implement {#export} to create an exporter
|
|
11
|
+
#
|
|
12
|
+
class Base
|
|
13
|
+
# @return [Schema] the schema to export
|
|
14
|
+
attr_reader :schema
|
|
15
|
+
|
|
16
|
+
# @return [Registry] the registry for resolving references
|
|
17
|
+
attr_reader :registry
|
|
18
|
+
|
|
19
|
+
# @return [Hash] export options
|
|
20
|
+
attr_reader :options
|
|
21
|
+
|
|
22
|
+
# Create a new exporter
|
|
23
|
+
#
|
|
24
|
+
# @param schema [Schema] the schema to export
|
|
25
|
+
# @param registry [Registry] the registry for resolving references
|
|
26
|
+
# @param options [Hash] export options
|
|
27
|
+
#
|
|
28
|
+
def initialize(schema, registry: nil, **options)
|
|
29
|
+
@schema = schema
|
|
30
|
+
@registry = registry
|
|
31
|
+
@options = options
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Export the schema to the target format
|
|
35
|
+
#
|
|
36
|
+
# @return [String] the exported content
|
|
37
|
+
# @abstract
|
|
38
|
+
#
|
|
39
|
+
def export
|
|
40
|
+
raise NotImplementedError, "Subclasses must implement #export"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Save export to a file
|
|
44
|
+
#
|
|
45
|
+
# @param path [String] file path
|
|
46
|
+
# @return [void]
|
|
47
|
+
#
|
|
48
|
+
def save(path)
|
|
49
|
+
File.write(path, export)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Get all fields including from nested schemas
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<Field>] all fields
|
|
57
|
+
#
|
|
58
|
+
def all_fields
|
|
59
|
+
schema.fields
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if a type is a schema reference
|
|
63
|
+
#
|
|
64
|
+
# @param type_name [String] the type name
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
#
|
|
67
|
+
def schema_reference?(type_name)
|
|
68
|
+
registry&.schema?(type_name) || false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get referenced schema
|
|
72
|
+
#
|
|
73
|
+
# @param type_name [String] the type name
|
|
74
|
+
# @return [Schema, nil]
|
|
75
|
+
#
|
|
76
|
+
def get_schema(type_name)
|
|
77
|
+
registry&.schema(type_name)
|
|
78
|
+
rescue KeyError
|
|
79
|
+
nil # schema not found
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
# :nocov:
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
# :nocov:
|
|
6
|
+
module Synthra
|
|
7
|
+
module Export
|
|
8
|
+
# Exports generated data as CSV
|
|
9
|
+
#
|
|
10
|
+
# @example Export schema data as CSV
|
|
11
|
+
# exporter = Csv.new(schema, registry: registry, count: 100)
|
|
12
|
+
# csv = exporter.export
|
|
13
|
+
#
|
|
14
|
+
class Csv < Base
|
|
15
|
+
def export
|
|
16
|
+
count = options[:count] || 10
|
|
17
|
+
seed = options[:seed]
|
|
18
|
+
mode = options[:mode] || :random
|
|
19
|
+
|
|
20
|
+
records = schema.generate_many(count, seed: seed, mode: mode, registry: registry)
|
|
21
|
+
|
|
22
|
+
return "" if records.empty?
|
|
23
|
+
|
|
24
|
+
# Flatten nested objects for CSV
|
|
25
|
+
flat_records = records.map { |r| flatten_record(r) }
|
|
26
|
+
|
|
27
|
+
# Get all possible headers
|
|
28
|
+
headers = flat_records.flat_map(&:keys).uniq.sort
|
|
29
|
+
|
|
30
|
+
CSV.generate do |csv|
|
|
31
|
+
csv << headers
|
|
32
|
+
flat_records.each do |record|
|
|
33
|
+
csv << headers.map { |h| format_value(record[h]) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Export with headers only (no data)
|
|
39
|
+
#
|
|
40
|
+
# @return [String] CSV header row
|
|
41
|
+
#
|
|
42
|
+
def export_headers
|
|
43
|
+
headers = schema.fields.map(&:name)
|
|
44
|
+
CSV.generate { |csv| csv << headers }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def flatten_record(record, prefix = "")
|
|
50
|
+
result = {}
|
|
51
|
+
|
|
52
|
+
record.each do |key, value|
|
|
53
|
+
full_key = prefix.empty? ? key : "#{prefix}.#{key}"
|
|
54
|
+
|
|
55
|
+
case value
|
|
56
|
+
when Hash
|
|
57
|
+
result.merge!(flatten_record(value, full_key))
|
|
58
|
+
when Array
|
|
59
|
+
if value.first.is_a?(Hash)
|
|
60
|
+
# Array of objects - just count them
|
|
61
|
+
result[full_key] = "[#{value.length} items]"
|
|
62
|
+
else
|
|
63
|
+
result[full_key] = value.join("; ")
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
result[full_key] = value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def format_value(value)
|
|
74
|
+
case value
|
|
75
|
+
when nil
|
|
76
|
+
""
|
|
77
|
+
when true
|
|
78
|
+
"true"
|
|
79
|
+
when false
|
|
80
|
+
"false"
|
|
81
|
+
when Array
|
|
82
|
+
escape_csv_formula(value.join("; "))
|
|
83
|
+
when Hash
|
|
84
|
+
escape_csv_formula(JSON.generate(value))
|
|
85
|
+
when String
|
|
86
|
+
escape_csv_formula(value)
|
|
87
|
+
else
|
|
88
|
+
value.to_s # numerics/dates are safe — never prefix them (would break -5, =NA values stay strings)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# SECURITY (CSV injection): a string value beginning with = + - @ tab or CR is treated as a
|
|
93
|
+
# formula by spreadsheet apps that open the export. Prefix it with a single quote so the cell
|
|
94
|
+
# is rendered as literal text. The gem ships a `hostile_payloads` type that emits exactly these.
|
|
95
|
+
def escape_csv_formula(str)
|
|
96
|
+
str.match?(/\A[=+\-@\t\r]/) ? "'#{str}" : str
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
# :nocov:
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthra
|
|
4
|
+
module Export
|
|
5
|
+
# GraphQL schema export
|
|
6
|
+
#
|
|
7
|
+
# Exports Synthra schemas to GraphQL SDL (Schema Definition Language).
|
|
8
|
+
#
|
|
9
|
+
# @example Export single schema
|
|
10
|
+
# exporter = Graphql.new(schema, registry: registry)
|
|
11
|
+
# graphql_schema = exporter.export
|
|
12
|
+
#
|
|
13
|
+
# @example Export all schemas
|
|
14
|
+
# graphql_schema = Graphql.export_all(registry)
|
|
15
|
+
#
|
|
16
|
+
class Graphql < Base
|
|
17
|
+
# Type mapping from Synthra types to GraphQL scalars
|
|
18
|
+
TYPE_MAP = {
|
|
19
|
+
"uuid" => "ID",
|
|
20
|
+
"ulid" => "ID",
|
|
21
|
+
"id_sequence" => "ID",
|
|
22
|
+
"snowflake_id" => "ID",
|
|
23
|
+
"numeric_string_id" => "ID",
|
|
24
|
+
"email" => "String",
|
|
25
|
+
"url" => "String",
|
|
26
|
+
"text" => "String",
|
|
27
|
+
"name" => "String",
|
|
28
|
+
"first_name" => "String",
|
|
29
|
+
"last_name" => "String",
|
|
30
|
+
"full_name" => "String",
|
|
31
|
+
"username" => "String",
|
|
32
|
+
"phone" => "String",
|
|
33
|
+
"address" => "String",
|
|
34
|
+
"city" => "String",
|
|
35
|
+
"state" => "String",
|
|
36
|
+
"country" => "String",
|
|
37
|
+
"country_code" => "String",
|
|
38
|
+
"postal_code" => "String",
|
|
39
|
+
"ip" => "String",
|
|
40
|
+
"ipv6" => "String",
|
|
41
|
+
"domain" => "String",
|
|
42
|
+
"user_agent" => "String",
|
|
43
|
+
"password" => "String",
|
|
44
|
+
"color" => "String",
|
|
45
|
+
"hex_color" => "String",
|
|
46
|
+
"number" => "Int",
|
|
47
|
+
"integer" => "Int",
|
|
48
|
+
"age" => "Int",
|
|
49
|
+
"airport_elevation" => "Int",
|
|
50
|
+
"discount" => "Int",
|
|
51
|
+
"formula" => "Int",
|
|
52
|
+
"row_number" => "Int",
|
|
53
|
+
"sequence" => "Int",
|
|
54
|
+
"float" => "Float",
|
|
55
|
+
"money" => "Float",
|
|
56
|
+
"latitude" => "Float",
|
|
57
|
+
"longitude" => "Float",
|
|
58
|
+
"airport_latitude" => "Float",
|
|
59
|
+
"airport_longitude" => "Float",
|
|
60
|
+
"product_price" => "Float",
|
|
61
|
+
"tax_rate" => "Float",
|
|
62
|
+
"transaction_amount" => "Float",
|
|
63
|
+
"boolean" => "Boolean",
|
|
64
|
+
"date" => "String",
|
|
65
|
+
"date_between" => "String",
|
|
66
|
+
"past_date" => "String",
|
|
67
|
+
"future_date" => "String",
|
|
68
|
+
"mobile_device_release_date" => "String",
|
|
69
|
+
"timestamp" => "String",
|
|
70
|
+
"datetime" => "String",
|
|
71
|
+
"now" => "String",
|
|
72
|
+
"time" => "String"
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
# Export single schema to GraphQL
|
|
76
|
+
#
|
|
77
|
+
# @return [String] GraphQL type definition
|
|
78
|
+
#
|
|
79
|
+
def export
|
|
80
|
+
lines = []
|
|
81
|
+
|
|
82
|
+
# Add custom scalar definitions
|
|
83
|
+
lines << "# Synthra Generated GraphQL Schema"
|
|
84
|
+
lines << "# Generated at: #{Time.now.iso8601}"
|
|
85
|
+
lines << ""
|
|
86
|
+
|
|
87
|
+
# Build type
|
|
88
|
+
lines << build_type(@schema)
|
|
89
|
+
|
|
90
|
+
lines.join("\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Export all schemas to GraphQL
|
|
94
|
+
#
|
|
95
|
+
# @param registry [Registry] schema registry
|
|
96
|
+
# @return [String] complete GraphQL schema
|
|
97
|
+
#
|
|
98
|
+
def self.export_all(registry)
|
|
99
|
+
lines = []
|
|
100
|
+
lines << "# Synthra Generated GraphQL Schema"
|
|
101
|
+
lines << "# Generated at: #{Time.now.iso8601}"
|
|
102
|
+
lines << ""
|
|
103
|
+
|
|
104
|
+
enums = []
|
|
105
|
+
types = []
|
|
106
|
+
|
|
107
|
+
registry.schemas.each_value do |schema|
|
|
108
|
+
exporter = new(schema, registry: registry)
|
|
109
|
+
types << exporter.build_type(schema)
|
|
110
|
+
enums.concat(exporter.collect_enums(schema))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Add enums first
|
|
114
|
+
enums.uniq.each do |enum_def|
|
|
115
|
+
lines << enum_def
|
|
116
|
+
lines << ""
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Add types
|
|
120
|
+
types.each do |type_def|
|
|
121
|
+
lines << type_def
|
|
122
|
+
lines << ""
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Add Query type
|
|
126
|
+
lines << build_query_type(registry)
|
|
127
|
+
|
|
128
|
+
lines.join("\n")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Build a GraphQL type from a schema
|
|
132
|
+
#
|
|
133
|
+
# @param schema [Schema] the schema to convert
|
|
134
|
+
# @return [String] GraphQL type definition
|
|
135
|
+
#
|
|
136
|
+
def build_type(schema)
|
|
137
|
+
lines = []
|
|
138
|
+
|
|
139
|
+
# Add description if available
|
|
140
|
+
if schema.deprecated?
|
|
141
|
+
lines << "\"\"\"#{schema.deprecation_message || 'This type is deprecated'}\"\"\""
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
lines << "type #{schema.name} {"
|
|
145
|
+
|
|
146
|
+
schema.fields.each do |field|
|
|
147
|
+
field_def = build_field(field)
|
|
148
|
+
lines << " #{field_def}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
lines << "}"
|
|
152
|
+
|
|
153
|
+
lines.join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Collect enum definitions from a schema
|
|
157
|
+
#
|
|
158
|
+
# @param schema [Schema] the schema to scan
|
|
159
|
+
# @return [Array<String>] enum definitions
|
|
160
|
+
#
|
|
161
|
+
def collect_enums(schema)
|
|
162
|
+
enums = []
|
|
163
|
+
|
|
164
|
+
schema.fields.each do |field|
|
|
165
|
+
if field.type_name == "enum"
|
|
166
|
+
enum_def = build_enum(schema.name, field)
|
|
167
|
+
enums << enum_def if enum_def
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
enums
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def build_field(field)
|
|
177
|
+
graphql_type = field_to_graphql_type(field)
|
|
178
|
+
nullable = field.optional? || field.nullable?
|
|
179
|
+
required_marker = nullable ? "" : "!"
|
|
180
|
+
|
|
181
|
+
deprecated = ""
|
|
182
|
+
if field.behaviors.any? { |b| b[:name] == :deprecated }
|
|
183
|
+
deprecated = " @deprecated"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
"#{field.name}: #{graphql_type}#{required_marker}#{deprecated}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def field_to_graphql_type(field)
|
|
190
|
+
type_name = field.type_name
|
|
191
|
+
|
|
192
|
+
# Handle enums
|
|
193
|
+
if type_name == "enum"
|
|
194
|
+
return enum_type_name(field)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Handle const
|
|
198
|
+
if type_name == "const"
|
|
199
|
+
value = field.type_args[:value]
|
|
200
|
+
return case value
|
|
201
|
+
when String then "String"
|
|
202
|
+
when Integer then "Int"
|
|
203
|
+
when Float then "Float"
|
|
204
|
+
when TrueClass, FalseClass then "Boolean"
|
|
205
|
+
else "String"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Handle arrays. The element is either a scalar type (map it; unknown scalars → String,
|
|
210
|
+
# matching the scalar fallback) or a schema reference (a PascalCase schema name — keep it).
|
|
211
|
+
if type_name == "array"
|
|
212
|
+
element_str = field.type_args[:element].to_s
|
|
213
|
+
element_graphql = TYPE_MAP[element_str] || (element_str.match?(/\A[A-Z]/) ? element_str : "String")
|
|
214
|
+
return "[#{element_graphql}!]"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Handle schema references
|
|
218
|
+
if @registry&.schema?(type_name)
|
|
219
|
+
return type_name
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Handle known types
|
|
223
|
+
TYPE_MAP[type_name] || "String"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def enum_type_name(field)
|
|
227
|
+
# Generate enum type name from field context
|
|
228
|
+
"#{@schema.name}#{field.name.split('_').map(&:capitalize).join}Enum"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_enum(schema_name, field)
|
|
232
|
+
enum_name = "#{schema_name}#{field.name.split('_').map(&:capitalize).join}Enum"
|
|
233
|
+
values = field.type_args[:values]
|
|
234
|
+
|
|
235
|
+
return nil unless values&.any?
|
|
236
|
+
|
|
237
|
+
lines = ["enum #{enum_name} {"]
|
|
238
|
+
|
|
239
|
+
values.each do |val|
|
|
240
|
+
value_str = val.respond_to?(:value) ? val.value : val.to_s
|
|
241
|
+
# GraphQL enum values must be SCREAMING_SNAKE_CASE
|
|
242
|
+
graphql_value = value_str.to_s.upcase.gsub(/[^A-Z0-9_]/, "_")
|
|
243
|
+
lines << " #{graphql_value}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
lines << "}"
|
|
247
|
+
lines.join("\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.build_query_type(registry)
|
|
251
|
+
lines = ["type Query {"]
|
|
252
|
+
|
|
253
|
+
registry.schemas.each do |name, _schema|
|
|
254
|
+
singular = name.downcase
|
|
255
|
+
plural = "#{singular}s"
|
|
256
|
+
|
|
257
|
+
lines << " #{singular}(id: ID!): #{name}"
|
|
258
|
+
lines << " #{plural}(limit: Int = 10, offset: Int = 0): [#{name}!]!"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
lines << "}"
|
|
262
|
+
lines.join("\n")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|