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,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Factory Bot Integration
5
+ #
6
+ # Provides seamless integration between Synthra and Factory Bot.
7
+ # Define factories using Synthra schemas with full trait support.
8
+ #
9
+ # @example Define a factory
10
+ # Synthra.define_factory(:user, schema: "User") do
11
+ # trait(:admin) do
12
+ # role { "admin" }
13
+ # end
14
+ #
15
+ # trait(:with_address) do
16
+ # address { Synthra.generate("Address") }
17
+ # end
18
+ # end
19
+ #
20
+ # @example Use in tests
21
+ # create(:user)
22
+ # create(:user, :admin)
23
+ # create(:user, :admin, :with_address, name: "Custom Name")
24
+ #
25
+ module FactoryBotIntegration
26
+ class << self
27
+ # Registry of Synthra factories
28
+ attr_accessor :factories
29
+
30
+ # Define a factory using a Synthra schema
31
+ #
32
+ # @param factory_name [Symbol] the factory name
33
+ # @param schema [String] the schema name
34
+ # @yield block for defining traits and overrides
35
+ # @return [void]
36
+ #
37
+ def define_factory(factory_name, schema:, &block)
38
+ @factories ||= {}
39
+ @factories[factory_name] = {
40
+ schema: schema,
41
+ traits: {},
42
+ overrides: {},
43
+ block: block
44
+ }
45
+
46
+ # Register with Factory Bot if available
47
+ register_with_factory_bot(factory_name, schema, &block) if factory_bot_available?
48
+ end
49
+
50
+ # Generate data using a defined factory
51
+ #
52
+ # @param factory_name [Symbol] the factory name
53
+ # @param traits [Array<Symbol>] traits to apply
54
+ # @param overrides [Hash] field overrides
55
+ # @return [Hash] generated data
56
+ #
57
+ def build(factory_name, *traits, **overrides)
58
+ factory = @factories&.[](factory_name)
59
+ raise ArgumentError, "Unknown factory: #{factory_name}" unless factory
60
+
61
+ # Get the schema
62
+ schema = Synthra.parse(load_schema_source(factory[:schema]))
63
+
64
+ # Apply traits
65
+ merged_overrides = overrides.dup
66
+ traits.each do |trait_name|
67
+ trait = factory[:traits][trait_name]
68
+ merged_overrides.merge!(trait) if trait
69
+ end
70
+
71
+ # Generate with overrides
72
+ schema.generate(overrides: merged_overrides)
73
+ end
74
+
75
+ # Check if Factory Bot is available
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ def factory_bot_available?
80
+ !!defined?(FactoryBot)
81
+ end
82
+
83
+ private
84
+
85
+ def load_schema_source(schema_name)
86
+ # Check if it's already a schema object or a string
87
+ if schema_name.is_a?(Schema)
88
+ return schema_name
89
+ end
90
+
91
+ # Try to find in default registry
92
+ if Synthra.instance_variable_get(:@default_registry)
93
+ registry = Synthra.instance_variable_get(:@default_registry)
94
+ return registry.schema(schema_name) if registry.schema?(schema_name)
95
+ end
96
+
97
+ # Create minimal DSL for the schema
98
+ "#{schema_name}:\n id: uuid"
99
+ end
100
+
101
+ # :nocov: requires the factory_bot gem's DSL runtime (FactoryBot.define / factory / trait),
102
+ # which is not a dependency of this gem; exercised only inside a consuming app that has
103
+ # factory_bot loaded. The dispatch into here is covered (see define_factory spec).
104
+ def register_with_factory_bot(factory_name, schema_name, &block)
105
+ # Integration with Factory Bot DSL
106
+ factory_config = @factories[factory_name]
107
+
108
+ FactoryBot.define do
109
+ factory factory_name do
110
+ initialize_with do
111
+ Synthra::FactoryBotIntegration.build(
112
+ factory_name,
113
+ *@build_strategy_traits,
114
+ **attributes
115
+ )
116
+ end
117
+
118
+ # Process the block to extract traits
119
+ instance_exec(&block) if block
120
+ end
121
+ end
122
+ end
123
+ # :nocov:
124
+ end
125
+
126
+ # DSL methods available inside factory definition blocks
127
+ module DSL
128
+ # Define a trait for the factory
129
+ #
130
+ # @param name [Symbol] trait name
131
+ # @yield block that returns field overrides
132
+ #
133
+ def trait(name, &block)
134
+ # Store the trait for later use
135
+ @current_factory_traits ||= {}
136
+ @current_factory_traits[name] = block
137
+ end
138
+ end
139
+
140
+ # RSpec integration helpers
141
+ module RSpec
142
+ # Include Synthra factory support in RSpec
143
+ #
144
+ # @example In spec_helper.rb
145
+ # require 'synthra/factory_bot_integration'
146
+ # RSpec.configure do |config|
147
+ # config.include Synthra::FactoryBotIntegration::RSpec
148
+ # end
149
+ #
150
+ def fake_data_build(factory_name, *traits, **overrides)
151
+ FactoryBotIntegration.build(factory_name, *traits, **overrides)
152
+ end
153
+
154
+ alias build_fake_data fake_data_build
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,440 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Field and FieldBehavior Classes
6
+ # =============================================================================
7
+ #
8
+ # This file defines the Field class which represents a single field in a
9
+ # schema, and the FieldBehavior class for field-level behaviors.
10
+ #
11
+ # Fields are created during DSL parsing and contain all information needed
12
+ # to generate values: type, arguments, modifiers, and behaviors.
13
+ #
14
+ # @example Field in DSL
15
+ # User:
16
+ # name: text # Simple field
17
+ # age: number(18..80) # Field with arguments
18
+ # email?: email # Optional field (? suffix)
19
+ # status: enum(active, inactive) @partial_data 20% # Field with behavior
20
+ #
21
+ # =============================================================================
22
+
23
+
24
+ module Synthra
25
+
26
+ # Represents a single field in a schema definition
27
+ #
28
+ # A Field encapsulates all information about a schema field including:
29
+ # - Field name and whether it's optional
30
+ # - Type name and arguments for generation
31
+ # - Nullable status (can the value be nil?)
32
+ # - Conditional expression (when should this field be included?)
33
+ # - Field-level behaviors (latency, partial_data, etc.)
34
+ #
35
+ # @example Create a field programmatically
36
+ # field = Field.new(
37
+ # name: "email",
38
+ # type_name: "email",
39
+ # type_args: { domain: "example.com" },
40
+ # nullable: false
41
+ # )
42
+ #
43
+ # @example Optional field (from DSL: "nickname?: text")
44
+ # field = Field.new(name: "nickname?", type_name: "text")
45
+ # field.name # => "nickname"
46
+ # field.optional? # => true
47
+ #
48
+ # @example Nullable type (from DSL: "manager: User?")
49
+ # field = Field.new(name: "manager", type_name: "User?")
50
+ # field.nullable? # => true
51
+ #
52
+
53
+ class Field
54
+
55
+ # @!attribute [r] name
56
+ # The field name (without ? suffix)
57
+ # @return [String] the clean field name
58
+
59
+ attr_reader :name
60
+
61
+
62
+ # @!attribute [r] type_name
63
+ # The type name (without ? suffix)
64
+ # @return [String] the clean type name
65
+
66
+ attr_reader :type_name
67
+
68
+
69
+ # @!attribute [r] type_args
70
+ # Arguments passed to the type generator
71
+ # @return [Hash] type arguments (e.g., { min: 0, max: 100 })
72
+
73
+ attr_reader :type_args
74
+
75
+
76
+ # @!attribute [r] condition
77
+ # Field name for conditional inclusion
78
+ # @return [String, nil] the condition field name, or nil if unconditional
79
+
80
+ attr_reader :condition
81
+
82
+
83
+ # @!attribute [r] behaviors
84
+ # Field-level behaviors (latency, partial_data, etc.)
85
+ # @return [Array<FieldBehavior>] list of field behaviors
86
+
87
+ attr_reader :behaviors
88
+
89
+
90
+ # Create a new Field instance
91
+ #
92
+ # @param name [String] field name (may include ? suffix for optional)
93
+ # @param type_name [String] the type name (may include ? suffix for nullable)
94
+ # @param type_args [Hash] arguments to pass to the type generator
95
+ # @param nullable [Boolean] whether the field value can be nil
96
+ # @param condition [String, nil] field name that must be truthy for inclusion
97
+ # @param behaviors [Array<FieldBehavior>] field-level behaviors to apply
98
+ #
99
+ # @example Simple field
100
+ # Field.new(name: "id", type_name: "uuid")
101
+ #
102
+ # @example Field with arguments
103
+ # Field.new(
104
+ # name: "age",
105
+ # type_name: "number",
106
+ # type_args: { min: 18, max: 80 }
107
+ # )
108
+ #
109
+ # @example Optional nullable field with condition
110
+ # Field.new(
111
+ # name: "manager?",
112
+ # type_name: "User?",
113
+ # condition: "is_employee"
114
+ # )
115
+ #
116
+
117
+ def initialize(name:, type_name:, type_args: {}, nullable: false, condition: nil, behaviors: [])
118
+ @raw_name = name
119
+ @name = name.delete_suffix("?")
120
+ @optional = name.end_with?("?")
121
+ @type_name = type_name.to_s.delete_suffix("?")
122
+ @type_args = type_args || {}
123
+ @nullable = nullable || type_name.to_s.end_with?("?")
124
+ @condition = condition
125
+ @behaviors = behaviors
126
+ end
127
+
128
+
129
+ # Check if this field is optional
130
+ #
131
+ # Optional fields (marked with ? suffix in DSL) may be omitted from
132
+ # the generated output entirely based on configuration or randomness.
133
+ #
134
+ # @return [Boolean] true if the field is optional
135
+ #
136
+ # @example
137
+ # Field.new(name: "nickname?", type_name: "text").optional? # => true
138
+ # Field.new(name: "name", type_name: "text").optional? # => false
139
+ #
140
+
141
+ def optional?
142
+ @optional
143
+ end
144
+
145
+
146
+ # Check if this field's type is nullable
147
+ #
148
+ # Nullable types (marked with ? suffix on type in DSL) can generate
149
+ # nil values. This is different from optional - nullable fields are
150
+ # always included but may have nil values.
151
+ #
152
+ # @return [Boolean] true if the type can be nil
153
+ #
154
+ # @example
155
+ # Field.new(name: "manager", type_name: "User?").nullable? # => true
156
+ # Field.new(name: "manager", type_name: "User").nullable? # => false
157
+ #
158
+
159
+ def nullable?
160
+ @nullable
161
+ end
162
+
163
+
164
+ # Check if this field has a condition
165
+ #
166
+ # Conditional fields are only included when another field has a truthy
167
+ # value. The condition is specified with "if field_name" in the DSL.
168
+ #
169
+ # @return [Boolean] true if the field has a condition
170
+ #
171
+ # @example DSL: "admin_note: text if is_admin"
172
+ # field.conditional? # => true
173
+ # field.condition # => "is_admin"
174
+ #
175
+
176
+ def conditional?
177
+ !condition.nil?
178
+ end
179
+
180
+
181
+ # Check if this field has a uniqueness constraint
182
+ #
183
+ # Unique fields will not generate duplicate values within a batch.
184
+ # The generator will retry up to max_unique_retries times.
185
+ #
186
+ # @return [Boolean] true if the field requires unique values
187
+ #
188
+ # @example DSL: "id: uuid unique"
189
+ # field.unique? # => true
190
+ #
191
+
192
+ def unique?
193
+ type_args[:unique] == true
194
+ end
195
+
196
+
197
+ # Get the range constraint if present
198
+ #
199
+ # Range constraints limit the generated values to a specific range.
200
+ # Common for number types.
201
+ #
202
+ # @return [Range, nil] the range constraint, or nil if not specified
203
+ #
204
+ # @example DSL: "age: number(18..80)"
205
+ # field.range # => 18..80
206
+ #
207
+
208
+ def range
209
+ return nil unless type_args[:range]
210
+
211
+ type_args[:range]
212
+ end
213
+
214
+
215
+ # Check if this field has a specific behavior
216
+ #
217
+ # @param type [Symbol, String] the behavior type to check for
218
+ # @return [Boolean] true if the field has the specified behavior
219
+ #
220
+ # @example
221
+ # field.has_behavior?(:latency) # => true
222
+ # field.has_behavior?(:partial_data) # => false
223
+ #
224
+
225
+ def has_behavior?(type)
226
+ behaviors.any? { |b| b.type == type.to_sym }
227
+ end
228
+
229
+
230
+ # Get a specific behavior by type
231
+ #
232
+ # @param type [Symbol, String] the behavior type to retrieve
233
+ # @return [FieldBehavior, nil] the behavior or nil if not found
234
+ #
235
+ # @example
236
+ # behavior = field.behavior(:latency)
237
+ # behavior.value # => { min: 100, max: 500 }
238
+ #
239
+
240
+ def behavior(type)
241
+ behaviors.find { |b| b.type == type.to_sym }
242
+ end
243
+
244
+
245
+ # Get all modifiers as a hash
246
+ #
247
+ # Returns a hash containing all field modifiers (unique, range,
248
+ # nullable, optional) with their values, excluding nil values.
249
+ #
250
+ # @return [Hash] modifier name => value pairs
251
+ #
252
+ # @example
253
+ # field.modifiers
254
+ # # => { unique: true, nullable: false, optional: true }
255
+ #
256
+
257
+ def modifiers
258
+ {
259
+ unique: unique?,
260
+ range: range,
261
+ nullable: nullable?,
262
+ optional: optional?
263
+ }.compact
264
+ end
265
+
266
+
267
+ # String representation of the field
268
+ #
269
+ # Returns a human-readable string showing the field definition
270
+ # similar to how it would appear in a DSL file.
271
+ #
272
+ # @return [String] string representation
273
+ #
274
+ # @example
275
+ # field.to_s # => "name: text"
276
+ # field.to_s # => "age: number({min: 18, max: 80})"
277
+ #
278
+
279
+ def to_s
280
+ parts = ["#{@raw_name}: #{type_name}"]
281
+ parts << "(#{type_args})" unless type_args.empty?
282
+ parts << "if #{condition}" if condition
283
+ behaviors.each { |b| parts << "@#{b.type} #{b.value}" }
284
+ parts.join(" ")
285
+ end
286
+
287
+
288
+ # Inspect representation for debugging
289
+ #
290
+ # @return [String] inspect string
291
+ #
292
+
293
+ def inspect
294
+ "#<Field #{self}>"
295
+ end
296
+ end
297
+
298
+
299
+ # Represents a field-level behavior
300
+ #
301
+ # Field behaviors modify how individual fields are generated or returned.
302
+ # They can add latency, omit data, or apply other transformations to
303
+ # specific fields rather than the entire schema.
304
+ #
305
+ # @example DSL field with behavior
306
+ # # User:
307
+ # # profile_image: url @partial_data 30% # 30% chance of null
308
+ # # bio: text @latency 100..500ms # 100-500ms delay
309
+ #
310
+ # @example Create a field behavior programmatically
311
+ # behavior = FieldBehavior.new(
312
+ # type: :latency,
313
+ # value: { min: 100, max: 500 }
314
+ # )
315
+ #
316
+
317
+ class FieldBehavior
318
+
319
+ # @!attribute [r] type
320
+ # The behavior type (:latency, :partial_data, etc.)
321
+ # @return [Symbol] behavior type
322
+
323
+ attr_reader :type
324
+
325
+
326
+ # @!attribute [r] value
327
+ # The behavior configuration value
328
+ # @return [Integer, Range, Hash] behavior value/configuration
329
+
330
+ attr_reader :value
331
+
332
+
333
+ # Create a new FieldBehavior
334
+ #
335
+ # @param type [Symbol, String] behavior type (:partial_data, :latency, etc.)
336
+ # @param value [Integer, Range, Hash] behavior configuration
337
+ #
338
+ # @example Percentage-based behavior
339
+ # FieldBehavior.new(type: :partial_data, value: 30)
340
+ #
341
+ # @example Range-based behavior
342
+ # FieldBehavior.new(type: :latency, value: { min: 100, max: 500 })
343
+ #
344
+
345
+ def initialize(type:, value:)
346
+ @type = type.to_sym
347
+ @value = value
348
+ end
349
+
350
+
351
+ # Get the behavior value as a percentage
352
+ #
353
+ # For probability-based behaviors, extracts the percentage value
354
+ # from various value formats.
355
+ #
356
+ # @return [Integer] the percentage (0-100)
357
+ #
358
+ # @example
359
+ # FieldBehavior.new(type: :partial_data, value: 30).percentage
360
+ # # => 30
361
+ #
362
+ # FieldBehavior.new(type: :partial_data, value: { probability: 25 }).percentage
363
+ # # => 25
364
+ #
365
+
366
+ def percentage
367
+ case value
368
+ when Integer then value
369
+ when Hash then value[:percent] || value[:probability] || 0
370
+ else 0
371
+ end
372
+ end
373
+
374
+
375
+ # Get the behavior value as a range
376
+ #
377
+ # For range-based behaviors (like latency), converts various value
378
+ # formats into a Range object.
379
+ #
380
+ # @return [Range] the range
381
+ #
382
+ # @example
383
+ # FieldBehavior.new(type: :latency, value: 100..500).range
384
+ # # => 100..500
385
+ #
386
+ # FieldBehavior.new(type: :latency, value: { min: 100, max: 500 }).range
387
+ # # => 100..500
388
+ #
389
+ # FieldBehavior.new(type: :latency, value: 100).range
390
+ # # => 100..100
391
+ #
392
+
393
+ def range
394
+ case value
395
+ when Range then value
396
+ when Hash
397
+ min = value[:min] || 0
398
+ max = value[:max] || min
399
+ min..max
400
+ else
401
+ value..value
402
+ end
403
+ end
404
+
405
+
406
+ # String representation
407
+ #
408
+ # @return [String] string like "@latency 100..500"
409
+ #
410
+
411
+ def to_s
412
+ "@#{type} #{value}"
413
+ end
414
+
415
+
416
+ # Inspect representation
417
+ #
418
+ # @return [String] inspect string
419
+ #
420
+
421
+ def inspect
422
+ "#<FieldBehavior #{self}>"
423
+ end
424
+
425
+
426
+ # Check equality with another FieldBehavior
427
+ #
428
+ # Two behaviors are equal if they have the same type and value.
429
+ #
430
+ # @param other [FieldBehavior] the other behavior to compare
431
+ # @return [Boolean] true if equal
432
+ #
433
+
434
+ def ==(other)
435
+ return false unless other.is_a?(FieldBehavior)
436
+
437
+ type == other.type && value == other.value
438
+ end
439
+ end
440
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ module Functions
5
+
6
+ # Registry for custom functions used in computed fields
7
+ class Registry
8
+ @functions = {}
9
+ @mutex = Mutex.new
10
+
11
+ class << self
12
+
13
+ # Register a custom function
14
+ #
15
+ # @param name [Symbol] function name
16
+ # @param force [Boolean] allow overwriting existing function
17
+ # @yield [context] block that computes the value
18
+ # @return [void]
19
+ # @raise [ArgumentError] if function exists and force is false
20
+
21
+ def register(name, force: false, &block)
22
+ @mutex.synchronize do
23
+ key = name.to_sym
24
+ if @functions.key?(key) && !force
25
+ raise ArgumentError, "Function '#{name}' already registered. Use force: true to override."
26
+ end
27
+
28
+ @functions[key] = block
29
+ end
30
+ end
31
+
32
+
33
+ # Lookup a registered function
34
+ #
35
+ # @param name [Symbol] function name
36
+ # @return [Proc]
37
+ # @raise [MissingFunctionError] if not found
38
+
39
+ def lookup(name)
40
+ @mutex.synchronize do
41
+ @functions.fetch(name.to_sym) do
42
+ raise MissingFunctionError, name
43
+ end
44
+ end
45
+ end
46
+
47
+
48
+ # Call a registered function
49
+ #
50
+ # @param name [Symbol] function name
51
+ # @param context [Context] generation context
52
+ # @return [Object] result
53
+
54
+ def call(name, context)
55
+ func = lookup(name)
56
+ func.call(context)
57
+ end
58
+
59
+
60
+ # Check if a function is registered
61
+ #
62
+ # @param name [Symbol] function name
63
+ # @return [Boolean]
64
+
65
+ def registered?(name)
66
+ @mutex.synchronize do
67
+ @functions.key?(name.to_sym)
68
+ end
69
+ end
70
+
71
+
72
+ # List all registered function names
73
+ #
74
+ # @return [Array<Symbol>]
75
+
76
+ def names
77
+ @mutex.synchronize { @functions.keys }
78
+ end
79
+
80
+
81
+ # Clear all registered functions
82
+ #
83
+ # @return [void]
84
+
85
+ def clear
86
+ @mutex.synchronize { @functions.clear }
87
+ end
88
+
89
+
90
+ # Unregister a specific function
91
+ #
92
+ # @param name [Symbol] function name
93
+ # @return [Proc, nil] the removed function
94
+
95
+ def unregister(name)
96
+ @mutex.synchronize do
97
+ @functions.delete(name.to_sym)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+