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,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthra
4
+ # Time Travel for Data Generation
5
+ #
6
+ # Generate data as it would have existed at a specific point in time.
7
+ # Perfect for testing time-sensitive features, historical data analysis,
8
+ # and creating realistic time-series datasets.
9
+ #
10
+ # @example Generate data from the past
11
+ # Synthra::TimeTravel.at(3.months.ago) do
12
+ # user = schema.generate
13
+ # user["created_at"] # => 3 months ago
14
+ # end
15
+ #
16
+ # @example Generate data within a range
17
+ # Synthra::TimeTravel.between(1.year.ago, 1.month.ago) do
18
+ # orders = schema.generate_many(100)
19
+ # end
20
+ #
21
+ # @example In DSL
22
+ # HistoricalOrder:
23
+ # @time_travel -30.days..-1.day
24
+ # created_at: timestamp
25
+ #
26
+ module TimeTravel
27
+ class << self
28
+ # Thread-local time context
29
+ def current_time
30
+ Thread.current[:synthra_time_travel]
31
+ end
32
+
33
+ def current_time=(time)
34
+ Thread.current[:synthra_time_travel] = time
35
+ end
36
+
37
+ # The active time-travel anchor, as a Date, for generators that need to
38
+ # re-base a "relative to today" distribution (e.g. "N days in the past").
39
+ #
40
+ # Returns nil when no time-travel context is active, so callers can use
41
+ # it as a cheap presence check: `anchor = TimeTravel.anchor_date; return ... if anchor`.
42
+ #
43
+ # @return [Date, nil]
44
+ def anchor_date
45
+ context = current_time
46
+ return nil unless context
47
+
48
+ context.current_time.to_date
49
+ end
50
+
51
+ # Generate data at a specific point in time
52
+ #
53
+ # @param time [Time, DateTime, Date] the target time
54
+ # @yield block where all generated timestamps will be relative to this time
55
+ # @return [Object] result of the block
56
+ #
57
+ # @example
58
+ # Synthra::TimeTravel.at(2.weeks.ago) do
59
+ # user = Synthra[:User]
60
+ # user["created_at"] # => ~2 weeks ago
61
+ # end
62
+ #
63
+ def at(time, &block)
64
+ time = normalize_time(time)
65
+ with_time_context(TimeContext.new(time), &block)
66
+ end
67
+
68
+ # Generate data within a time range
69
+ #
70
+ # @param start_time [Time, DateTime, Date] range start
71
+ # @param end_time [Time, DateTime, Date] range end
72
+ # @yield block where timestamps will be randomly distributed in range
73
+ # @return [Object] result of the block
74
+ #
75
+ # @example
76
+ # Synthra::TimeTravel.between(1.year.ago, Time.now) do
77
+ # orders = Synthra.generate_many("Order", 1000)
78
+ # # created_at distributed over the year
79
+ # end
80
+ #
81
+ def between(start_time, end_time, &block)
82
+ start_time = normalize_time(start_time)
83
+ end_time = normalize_time(end_time)
84
+ with_time_context(TimeContext.new(start_time, end_time), &block)
85
+ end
86
+
87
+ # Generate historical time series data
88
+ #
89
+ # @param schema [Schema, String] schema to generate
90
+ # @param count [Integer] number of records
91
+ # @param start_time [Time] series start
92
+ # @param end_time [Time] series end (default: now)
93
+ # @param distribution [Symbol] :uniform, :linear, :exponential
94
+ # @return [Array<Hash>] generated records sorted by time
95
+ #
96
+ # @example Generate a year of orders
97
+ # orders = Synthra::TimeTravel.time_series(
98
+ # "Order",
99
+ # count: 1000,
100
+ # start_time: 1.year.ago,
101
+ # distribution: :exponential # More recent = more orders
102
+ # )
103
+ #
104
+ def time_series(schema, count:, start_time:, end_time: Time.now, distribution: :uniform)
105
+ schema = Synthra.registry.schema(schema.to_s) if schema.is_a?(String) || schema.is_a?(Symbol)
106
+
107
+ start_time = normalize_time(start_time)
108
+ end_time = normalize_time(end_time)
109
+ time_range = end_time - start_time
110
+
111
+ records = count.times.map do |i|
112
+ # Calculate time based on distribution
113
+ t = case distribution
114
+ when :uniform
115
+ random_time_in_range(start_time, end_time)
116
+ when :linear
117
+ # Linear growth - evenly distributed
118
+ start_time + (time_range * i / count.to_f)
119
+ when :exponential
120
+ # Exponential - more recent records
121
+ ratio = (i.to_f / count) ** 2
122
+ start_time + (time_range * ratio)
123
+ when :logarithmic
124
+ # Logarithmic - more historical records
125
+ ratio = Math.log(i + 1) / Math.log(count + 1)
126
+ start_time + (time_range * ratio)
127
+ else
128
+ random_time_in_range(start_time, end_time)
129
+ end
130
+
131
+ at(t) { schema.generate }
132
+ end
133
+
134
+ # Sort by timestamp fields
135
+ timestamp_field = find_timestamp_field(schema)
136
+ if timestamp_field
137
+ records.sort_by { |r| r[timestamp_field] || Time.now }
138
+ else
139
+ records
140
+ end
141
+ end
142
+
143
+ # Generate data that ages over time
144
+ #
145
+ # @param schema [Schema, String] schema to generate
146
+ # @param record [Hash] existing record to age
147
+ # @param by [Duration] how much to age
148
+ # @return [Hash] aged record
149
+ #
150
+ # @example Age a user by 1 year
151
+ # user = Synthra[:User]
152
+ # aged_user = Synthra::TimeTravel.age(user, by: 1.year)
153
+ #
154
+ def age(record, by:)
155
+ aged = record.dup
156
+ offset = duration_to_seconds(by)
157
+
158
+ record.each do |key, value|
159
+ if value.is_a?(Time) || value.is_a?(DateTime)
160
+ aged[key] = value - offset
161
+ elsif value.is_a?(String) && time_string?(value)
162
+ parsed = Time.parse(value)
163
+ aged[key] = (parsed - offset).iso8601
164
+ end
165
+ end
166
+
167
+ aged
168
+ end
169
+
170
+ # Create a progression of records over time
171
+ #
172
+ # @param schema [Schema, String] schema to generate
173
+ # @param seed_record [Hash] starting record
174
+ # @param steps [Integer] number of progression steps
175
+ # @param interval [Duration] time between steps
176
+ # @param changes [Hash] fields that change over time
177
+ # @yield [record, step, time] customize each step
178
+ # @return [Array<Hash>] progression of records
179
+ #
180
+ # @example User subscription progression
181
+ # progression = Synthra::TimeTravel.progression(
182
+ # "User",
183
+ # steps: 12,
184
+ # interval: 1.month,
185
+ # changes: {
186
+ # subscription_status: -> (step) { step < 3 ? "trial" : "active" },
187
+ # login_count: -> (step) { step * 5 }
188
+ # }
189
+ # )
190
+ #
191
+ def progression(schema, steps:, interval:, seed_record: nil, changes: {}, &block)
192
+ schema = Synthra.registry.schema(schema.to_s) if schema.is_a?(String) || schema.is_a?(Symbol)
193
+
194
+ interval_seconds = duration_to_seconds(interval)
195
+ base_time = current_time || Time.now
196
+ records = []
197
+
198
+ seed_record ||= schema.generate
199
+
200
+ steps.times do |step|
201
+ step_time = base_time + (interval_seconds * step)
202
+
203
+ record = at(step_time) do
204
+ step_record = seed_record.dup
205
+
206
+ # Apply time-based changes
207
+ changes.each do |field, change_fn|
208
+ step_record[field.to_s] = change_fn.call(step)
209
+ end
210
+
211
+ # Update timestamp fields
212
+ schema.fields.each do |field|
213
+ if timestamp_type?(field.type_name)
214
+ step_record[field.name] = step_time.iso8601
215
+ end
216
+ end
217
+
218
+ # Allow custom modifications
219
+ block&.call(step_record, step, step_time)
220
+
221
+ step_record
222
+ end
223
+
224
+ records << record
225
+ end
226
+
227
+ records
228
+ end
229
+
230
+ private
231
+
232
+ def with_time_context(context)
233
+ old_context = current_time
234
+ self.current_time = context
235
+ yield
236
+ ensure
237
+ self.current_time = old_context
238
+ end
239
+
240
+ def normalize_time(time)
241
+ case time
242
+ when Time
243
+ time
244
+ when DateTime
245
+ time.to_time
246
+ when Date
247
+ time.to_time
248
+ when Numeric
249
+ Time.at(time)
250
+ when Range
251
+ # Return as-is for range support
252
+ time
253
+ else
254
+ Time.parse(time.to_s)
255
+ end
256
+ end
257
+
258
+ def random_time_in_range(start_time, end_time)
259
+ range = end_time.to_f - start_time.to_f
260
+ Time.at(start_time.to_f + rand * range)
261
+ end
262
+
263
+ def find_timestamp_field(schema)
264
+ %w[created_at timestamp date created].each do |name|
265
+ return name if schema.fields.any? { |f| f.name == name }
266
+ end
267
+ nil
268
+ end
269
+
270
+ def duration_to_seconds(duration)
271
+ return duration if duration.is_a?(Numeric)
272
+
273
+ # Parse a "30.days"-style duration BEFORE any to_i fallback. A String responds to #to_i, so
274
+ # "30.days".to_i (=> 30) used to shadow this multiplier table — aging by 30 *seconds*
275
+ # instead of 30 days. Match on #to_s so a custom duration object works too.
276
+ if (match = duration.to_s.match(/(\d+)\.(year|month|week|day|hour|minute|second)s?/))
277
+ num, unit = match.captures
278
+ multipliers = {
279
+ "year" => 365.25 * 24 * 3600,
280
+ "month" => 30 * 24 * 3600,
281
+ "week" => 7 * 24 * 3600,
282
+ "day" => 24 * 3600,
283
+ "hour" => 3600,
284
+ "minute" => 60,
285
+ "second" => 1
286
+ }
287
+ return num.to_i * multipliers[unit]
288
+ end
289
+
290
+ return duration.to_i if duration.respond_to?(:to_i)
291
+
292
+ duration.to_f
293
+ end
294
+
295
+ def time_string?(value)
296
+ return false unless value.is_a?(String)
297
+ # Be strict about what constitutes a time string
298
+ # Must contain date-like patterns (YYYY-MM-DD, DD/MM/YYYY, etc.)
299
+ return false unless value.match?(/\d{4}-\d{2}-\d{2}|\d{2}[\/\-]\d{2}[\/\-]\d{2,4}|T\d{2}:\d{2}/)
300
+ Time.parse(value)
301
+ true
302
+ rescue ArgumentError, TypeError
303
+ false
304
+ end
305
+
306
+ def timestamp_type?(type_name)
307
+ %w[timestamp datetime now date time].include?(type_name)
308
+ end
309
+ end
310
+
311
+ # Time context for generation
312
+ class TimeContext
313
+ attr_reader :base_time, :end_time, :range
314
+
315
+ def initialize(base_time, end_time = nil)
316
+ @base_time = base_time
317
+ @end_time = end_time
318
+ @range = end_time ? (base_time..end_time) : nil
319
+ end
320
+
321
+ def current_time
322
+ if @range
323
+ # Random time in range
324
+ start_f = @base_time.to_f
325
+ end_f = @end_time.to_f
326
+ Time.at(start_f + rand * (end_f - start_f))
327
+ else
328
+ @base_time
329
+ end
330
+ end
331
+
332
+ def range?
333
+ !@range.nil?
334
+ end
335
+ end
336
+ end
337
+
338
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+ # Synthra Type Definitions
5
+ # =============================================================================
6
+ #
7
+ # This file registers all built-in type generators for Synthra.
8
+ # Types are organized by category for easier maintenance.
9
+ #
10
+ # =============================================================================
11
+
12
+ module Synthra
13
+ # Registers all built-in types
14
+ #
15
+ # This method is called automatically when the gem loads.
16
+ # @api private
17
+ #
18
+ def self.register_builtin_types!
19
+ register_core_types!
20
+ register_personal_types!
21
+ register_commerce_types!
22
+ register_finance_types!
23
+ register_address_types!
24
+ register_technology_types!
25
+ register_text_types!
26
+ register_media_types!
27
+ register_advanced_types!
28
+ end
29
+
30
+ class << self
31
+ private
32
+
33
+ def register_core_types!
34
+ # Require core type files
35
+ require_relative "types/core/identifiers"
36
+ require_relative "types/core/primitives"
37
+ require_relative "types/core/collections"
38
+ require_relative "types/core/references"
39
+ require_relative "types/core/sequences"
40
+ require_relative "types/core/defaults"
41
+ require_relative "types/core/enums"
42
+
43
+ # Identifiers
44
+ Types::Registry.register("uuid", Types::Core::Uuid)
45
+ Types::Registry.register("ulid", Types::Core::Ulid)
46
+ Types::Registry.register("id_sequence", Types::Core::IdSequence)
47
+ Types::Registry.register("snowflake_id", Types::Core::SnowflakeId)
48
+ Types::Registry.register("unix_timestamp_ms", Types::Core::UnixTimestampMs)
49
+ Types::Registry.register("numeric_string_id", Types::Core::NumericStringId)
50
+ Types::Registry.register("alphanumeric_id", Types::Core::AlphanumericId)
51
+ Types::Registry.register("locale", Types::Core::Locale)
52
+ Types::Registry.register("token", Types::Core::Token)
53
+
54
+ # Primitives
55
+ Types::Registry.register("text", Types::Core::Text)
56
+ Types::Registry.register("string", Types::Core::Text) # alias for OpenAPI/contract compatibility
57
+ Types::Registry.register("number", Types::Core::Number)
58
+ Types::Registry.register("boolean", Types::Core::Boolean)
59
+ Types::Registry.register("const", Types::Core::Const)
60
+
61
+ # Collections
62
+ Types::Registry.register("array", Types::Core::ArrayType)
63
+ Types::Registry.register("object", Types::Core::ObjectType)
64
+ Types::Registry.register("map_by_field", Types::Core::MapByField)
65
+ Types::Registry.register("one_of", Types::Core::OneOf)
66
+ Types::Registry.register("enum", Types::Core::Enum)
67
+
68
+ # References and sequences
69
+ Types::Registry.register("sequence", Types::Core::Sequence)
70
+ Types::Registry.register("default_value", Types::Core::DefaultValue)
71
+ Types::Registry.register("reference", Types::Core::Reference)
72
+ Types::Registry.register("custom", Types::Core::Custom)
73
+ Types::Registry.register("copy", Types::Core::Copy)
74
+ Types::Registry.register("shared", Types::Core::Shared)
75
+ Types::Registry.register("character_sequence", Types::Core::CharacterSequence)
76
+ Types::Registry.register("digit_sequence", Types::Core::DigitSequence)
77
+
78
+ # Simulyra DSL schema reference type
79
+ Types::Registry.register("schema_ref", Types::Core::SchemaRef)
80
+ end
81
+
82
+ def register_personal_types!
83
+ Types::Registry.register("name", Types::PersonalNames::Name)
84
+ Types::Registry.register("first_name", Types::PersonalNames::FirstName)
85
+ Types::Registry.register("last_name", Types::PersonalNames::LastName)
86
+ Types::Registry.register("full_name", Types::PersonalNames::FullName)
87
+ Types::Registry.register("gender", Types::PersonalNames::Gender)
88
+ Types::Registry.register("age", Types::PersonalNames::Age)
89
+ Types::Registry.register("username", Types::PersonalNames::Username)
90
+ Types::Registry.register("family_name_chinese", Types::PersonalNames::FamilyNameChinese)
91
+ Types::Registry.register("given_name_chinese", Types::PersonalNames::GivenNameChinese)
92
+ Types::Registry.register("first_name_european", Types::PersonalNames::FirstNameEuropean)
93
+ Types::Registry.register("first_name_female", Types::PersonalNames::FirstNameFemale)
94
+ Types::Registry.register("first_name_male", Types::PersonalNames::FirstNameMale)
95
+ Types::Registry.register("gender_abbrev", Types::PersonalNames::GenderAbbrev)
96
+ Types::Registry.register("gender_binary", Types::PersonalNames::GenderBinary)
97
+ Types::Registry.register("gender_facebook", Types::PersonalNames::GenderFacebook)
98
+ Types::Registry.register("suffix", Types::PersonalNames::Suffix)
99
+ Types::Registry.register("title", Types::PersonalNames::Title)
100
+ end
101
+
102
+ def register_commerce_types!
103
+ Types::Registry.register("job_title", Types::CommerceProducts::JobTitle)
104
+ Types::Registry.register("company_name", Types::CommerceProducts::CompanyName)
105
+ Types::Registry.register("product_name", Types::CommerceProducts::ProductName)
106
+ Types::Registry.register("product_category", Types::CommerceProducts::ProductCategory)
107
+ Types::Registry.register("sku", Types::CommerceProducts::Sku)
108
+ Types::Registry.register("discount", Types::CommerceProducts::Discount)
109
+ Types::Registry.register("tax_rate", Types::CommerceProducts::TaxRate)
110
+ Types::Registry.register("construction_heavy_equipment", Types::CommerceProducts::ConstructionHeavyEquipment)
111
+ Types::Registry.register("construction_material", Types::CommerceProducts::ConstructionMaterial)
112
+ Types::Registry.register("construction_role", Types::CommerceProducts::ConstructionRole)
113
+ Types::Registry.register("construction_standard_cost_code", Types::CommerceProducts::ConstructionStandardCostCode)
114
+ Types::Registry.register("construction_subcontract_category", Types::CommerceProducts::ConstructionSubcontractCategory)
115
+ Types::Registry.register("construction_trade", Types::CommerceProducts::ConstructionTrade)
116
+ Types::Registry.register("product_grocery", Types::CommerceProducts::ProductGrocery)
117
+ Types::Registry.register("product_description", Types::CommerceProducts::ProductDescription)
118
+ Types::Registry.register("product_price", Types::CommerceProducts::ProductPrice)
119
+ Types::Registry.register("product_subcategory", Types::CommerceProducts::ProductSubcategory)
120
+ Types::Registry.register("shirt_size", Types::CommerceProducts::ShirtSize)
121
+ end
122
+
123
+ def register_finance_types!
124
+ Types::Registry.register("money", Types::FinanceBanking::Money)
125
+ Types::Registry.register("currency", Types::FinanceBanking::Currency)
126
+ Types::Registry.register("currency_code", Types::FinanceBanking::Currency)
127
+ Types::Registry.register("credit_card", Types::FinanceBanking::CreditCard)
128
+ Types::Registry.register("bank_name", Types::FinanceBanking::BankName)
129
+ Types::Registry.register("account_number", Types::FinanceBanking::AccountNumber)
130
+ Types::Registry.register("iban", Types::FinanceBanking::Iban)
131
+ Types::Registry.register("transaction_amount", Types::FinanceBanking::TransactionAmount)
132
+ Types::Registry.register("ssn", Types::FinanceBanking::Ssn)
133
+ Types::Registry.register("ein", Types::FinanceBanking::Ein)
134
+ Types::Registry.register("bank_routing_number", Types::FinanceBanking::BankRoutingNumber)
135
+ Types::Registry.register("bank_swift_bic", Types::FinanceBanking::BankSwiftBic)
136
+ Types::Registry.register("bank_lei", Types::FinanceBanking::BankLei)
137
+ Types::Registry.register("bank_riad_code", Types::FinanceBanking::BankRiadCode)
138
+ Types::Registry.register("credit_card_type", Types::FinanceBanking::CreditCardType)
139
+ Types::Registry.register("duns_number", Types::FinanceBanking::DunsNumber)
140
+ Types::Registry.register("medicare_beneficiary_id", Types::FinanceBanking::MedicareBeneficiaryId)
141
+ Types::Registry.register("nhs_number", Types::FinanceBanking::NhsNumber)
142
+ end
143
+
144
+ def register_address_types!
145
+ Types::Registry.register("country_code", Types::AddressLocation::CountryCode)
146
+ Types::Registry.register("country", Types::AddressLocation::Country)
147
+ Types::Registry.register("city", Types::AddressLocation::City)
148
+ Types::Registry.register("state", Types::AddressLocation::State)
149
+ Types::Registry.register("address", Types::AddressLocation::Address)
150
+ Types::Registry.register("latitude", Types::AddressLocation::Latitude)
151
+ Types::Registry.register("longitude", Types::AddressLocation::Longitude)
152
+ Types::Registry.register("timezone", Types::AddressLocation::Timezone)
153
+ Types::Registry.register("postal_code", Types::AddressLocation::PostalCode)
154
+ Types::Registry.register("zip_code", Types::AddressLocation::PostalCode)
155
+ Types::Registry.register("address_line_2", Types::AddressLocation::AddressLine2)
156
+ Types::Registry.register("street_name", Types::AddressLocation::StreetName)
157
+ Types::Registry.register("street_address", Types::AddressLocation::StreetName)
158
+ Types::Registry.register("street_number", Types::AddressLocation::StreetNumber)
159
+ Types::Registry.register("street_suffix", Types::AddressLocation::StreetSuffix)
160
+ Types::Registry.register("airport_name", Types::AddressLocation::AirportName)
161
+ Types::Registry.register("airport_continent", Types::AddressLocation::AirportContinent)
162
+ Types::Registry.register("airport_country_code", Types::AddressLocation::AirportCountryCode)
163
+ Types::Registry.register("airport_elevation", Types::AddressLocation::AirportElevation)
164
+ Types::Registry.register("airport_gps_code", Types::AddressLocation::AirportGpsCode)
165
+ Types::Registry.register("airport_latitude", Types::AddressLocation::AirportLatitude)
166
+ Types::Registry.register("airport_longitude", Types::AddressLocation::AirportLongitude)
167
+ Types::Registry.register("airport_municipality", Types::AddressLocation::AirportMunicipality)
168
+ Types::Registry.register("airport_region_code", Types::AddressLocation::AirportRegionCode)
169
+ Types::Registry.register("bank_city", Types::AddressLocation::BankCity)
170
+ Types::Registry.register("bank_country_code", Types::AddressLocation::BankCountryCode)
171
+ Types::Registry.register("bank_street_address", Types::AddressLocation::BankStreetAddress)
172
+ Types::Registry.register("bank_state", Types::AddressLocation::BankState)
173
+ Types::Registry.register("hospital_city", Types::AddressLocation::HospitalCity)
174
+ Types::Registry.register("hospital_postal_code", Types::AddressLocation::HospitalPostalCode)
175
+ Types::Registry.register("hospital_state", Types::AddressLocation::HospitalState)
176
+ Types::Registry.register("hospital_street_address", Types::AddressLocation::HospitalStreetAddress)
177
+ end
178
+
179
+ def register_technology_types!
180
+ Types::Registry.register("email", Types::TechnologyInternet::Email)
181
+ Types::Registry.register("phone", Types::TechnologyInternet::Phone)
182
+ Types::Registry.register("url", Types::TechnologyInternet::Url)
183
+ Types::Registry.register("ip", Types::TechnologyInternet::Ip)
184
+ Types::Registry.register("ip_address", Types::TechnologyInternet::Ip)
185
+ Types::Registry.register("ipv6", Types::TechnologyInternet::Ipv6)
186
+ Types::Registry.register("mac_address", Types::TechnologyInternet::MacAddress)
187
+ Types::Registry.register("domain", Types::TechnologyInternet::Domain)
188
+ Types::Registry.register("user_agent", Types::TechnologyInternet::UserAgent)
189
+ Types::Registry.register("app_bundle_id", Types::TechnologyInternet::AppBundleId)
190
+ Types::Registry.register("app_name", Types::TechnologyInternet::AppName)
191
+ Types::Registry.register("app_version", Types::TechnologyInternet::AppVersion)
192
+ Types::Registry.register("base64_image_url", Types::TechnologyInternet::Base64ImageUrl)
193
+ Types::Registry.register("dummy_image_url", Types::TechnologyInternet::DummyImageUrl)
194
+ Types::Registry.register("ip_v4_cidr", Types::TechnologyInternet::IpV4Cidr)
195
+ Types::Registry.register("ip_v6_cidr", Types::TechnologyInternet::IpV6Cidr)
196
+ Types::Registry.register("language", Types::TechnologyInternet::Language)
197
+ Types::Registry.register("language_code", Types::TechnologyInternet::LanguageCode)
198
+ Types::Registry.register("mime_type", Types::TechnologyInternet::MimeType)
199
+ Types::Registry.register("mobile_device_brand", Types::TechnologyInternet::MobileDeviceBrand)
200
+ Types::Registry.register("mobile_device_model", Types::TechnologyInternet::MobileDeviceModel)
201
+ Types::Registry.register("mobile_device_os", Types::TechnologyInternet::MobileDeviceOs)
202
+ Types::Registry.register("mobile_device_release_date", Types::TechnologyInternet::MobileDeviceReleaseDate)
203
+ Types::Registry.register("mongodb_object_id", Types::TechnologyInternet::MongodbObjectId)
204
+ Types::Registry.register("top_level_domain", Types::TechnologyInternet::TopLevelDomain)
205
+ end
206
+
207
+ def register_text_types!
208
+ Types::Registry.register("avatar", Types::TextContent::Avatar)
209
+ Types::Registry.register("buzzword", Types::TextContent::Buzzword)
210
+ Types::Registry.register("catch_phrase", Types::TextContent::CatchPhrase)
211
+ Types::Registry.register("color", Types::TextContent::Color)
212
+ Types::Registry.register("hex_color", Types::TextContent::HexColor)
213
+ Types::Registry.register("short_hex_color", Types::TextContent::ShortHexColor)
214
+ Types::Registry.register("encrypt", Types::TextContent::Encrypt)
215
+ Types::Registry.register("file_name", Types::TextContent::FileName)
216
+ Types::Registry.register("frequency", Types::TextContent::Frequency)
217
+ Types::Registry.register("linkedin_skill", Types::TextContent::LinkedInSkill)
218
+ Types::Registry.register("md5", Types::TextContent::Md5)
219
+ Types::Registry.register("sha1", Types::TextContent::Sha1)
220
+ Types::Registry.register("sha256", Types::TextContent::Sha256)
221
+ Types::Registry.register("nato_phonetic", Types::TextContent::NatoPhonetic)
222
+ Types::Registry.register("paragraphs", Types::TextContent::Paragraphs)
223
+ Types::Registry.register("password", Types::TextContent::Password)
224
+ Types::Registry.register("password_hash", Types::TextContent::PasswordHash)
225
+ Types::Registry.register("row_number", Types::TextContent::RowNumber)
226
+ Types::Registry.register("sentences", Types::TextContent::Sentences)
227
+ Types::Registry.register("slogan", Types::TextContent::Slogan)
228
+ Types::Registry.register("sql_expression", Types::TextContent::SqlExpression)
229
+ Types::Registry.register("words", Types::TextContent::Words)
230
+ Types::Registry.register("slug", Types::TextContent::Slug)
231
+ Types::Registry.register("university", Types::TextContent::University)
232
+ Types::Registry.register("blank", Types::TextContent::Blank)
233
+ Types::Registry.register("race", Types::TextContent::Race)
234
+ Types::Registry.register("department_corporate", Types::TextContent::DepartmentCorporate)
235
+ Types::Registry.register("department_retail", Types::TextContent::DepartmentRetail)
236
+ Types::Registry.register("fake_company_name", Types::TextContent::FakeCompanyName)
237
+ Types::Registry.register("message_text", Types::TextContent::MessageText)
238
+ Types::Registry.register("empty_array", Types::TextContent::EmptyArray)
239
+ Types::Registry.register("hashtag_text", Types::TextContent::HashtagText)
240
+ Types::Registry.register("social_handle", Types::TextContent::SocialHandle)
241
+ Types::Registry.register("indices_pair", Types::TextContent::IndicesPair)
242
+ end
243
+
244
+ def register_media_types!
245
+ Types::Registry.register("movie_genres", Types::MediaEntertainment::MovieGenres)
246
+ Types::Registry.register("movie_title", Types::MediaEntertainment::MovieTitle)
247
+ end
248
+
249
+ def register_datetime_types!
250
+ Types::Registry.register("date", Types::DateTime::DateType)
251
+ Types::Registry.register("past_date", Types::DateTime::PastDate)
252
+ Types::Registry.register("future_date", Types::DateTime::FutureDate)
253
+ Types::Registry.register("now", Types::DateTime::Now)
254
+ Types::Registry.register("timestamp", Types::DateTime::Timestamp)
255
+ Types::Registry.register("time", Types::DateTime::TimeType)
256
+ Types::Registry.register("datetime", Types::DateTime::DatetimeType)
257
+ Types::Registry.register("date_between", Types::DateTime::DateBetween)
258
+ end
259
+
260
+ def register_advanced_types!
261
+ # Date/Time
262
+ register_datetime_types!
263
+
264
+ # Advanced types
265
+ Types::Registry.register("formula", Types::Formula)
266
+ Types::Registry.register("json_array", Types::JsonArray)
267
+ Types::Registry.register("naughty_string", Types::NaughtyString)
268
+ Types::Registry.register("regex", Types::Regex)
269
+ Types::Registry.register("repeating_element", Types::RepeatingElement)
270
+ Types::Registry.register("scenario", Types::Scenario)
271
+ Types::Registry.register("template", Types::Template)
272
+ end
273
+ end
274
+ end