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,1287 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =============================================================================
4
+
5
+ # Synthra Abstract Syntax Tree (AST) Nodes
6
+ # =============================================================================
7
+ #
8
+ # This file defines all AST node types used to represent the parsed
9
+ # structure of DSL files. The parser converts a token stream into
10
+ # a tree of these nodes.
11
+ #
12
+ # The AST serves as an intermediate representation between parsing
13
+ # and schema creation, enabling validation, transformation, and
14
+ # debugging of DSL definitions.
15
+ #
16
+ # Node Hierarchy:
17
+ # Node (base)
18
+ # ├── SchemaNode - Top-level schema definition
19
+ # ├── FieldNode - Field within a schema
20
+ # ├── TypeNode - Type reference (base for special types)
21
+ # │ ├── ArrayTypeNode - Array type with element and size
22
+ # │ └── CustomNode - Custom function call
23
+ # ├── BehaviorNode - Behavior annotation (@latency, etc.)
24
+ # ├── ConditionNode - Conditional field condition (if ...)
25
+ # ├── ReferenceNode - Cross-schema reference (Ref(User.id))
26
+ # ├── ModifierNode - Field modifier (unique, range)
27
+ # └── EnumValueNode - Enum value with optional probability
28
+ #
29
+ # =============================================================================
30
+
31
+
32
+ module Synthra
33
+ module Parser
34
+
35
+ # Abstract Syntax Tree nodes for representing parsed DSL structure
36
+ #
37
+ # The AST module contains node classes that represent the hierarchical
38
+ # structure of a parsed DSL file. Each node type corresponds to a
39
+ # different DSL construct.
40
+ #
41
+ # @example Parse DSL to AST
42
+ # parser = Synthra::Parser::Parser.new
43
+ # ast_nodes = parser.parse(dsl_source)
44
+ # # => [SchemaNode, SchemaNode, ...]
45
+ #
46
+ # @example Convert AST to Schema
47
+ # schema = ast_nodes.first.to_schema
48
+ # # => Synthra::Schema
49
+ #
50
+
51
+ module AST
52
+
53
+ # Base class for all AST nodes
54
+ #
55
+ # All AST nodes inherit from Node and share the `line` attribute
56
+ # for error reporting and debugging.
57
+ #
58
+ # @abstract Subclass and implement specific node behavior
59
+ #
60
+ # @!attribute [r] line
61
+ # Source line number where this node was defined
62
+ # @return [Integer, nil] the line number (1-based) or nil
63
+ #
64
+
65
+ class Node
66
+ attr_reader :line
67
+
68
+ # Create a new AST node
69
+ #
70
+ # @param line [Integer, nil] source line number (1-based)
71
+ #
72
+
73
+ def initialize(line: nil)
74
+ @line = line
75
+ end
76
+ end
77
+
78
+ # Schema definition node
79
+ #
80
+ # Represents a complete schema definition in the DSL. A SchemaNode
81
+ # contains the schema name, list of fields, and any schema-level
82
+ # behaviors.
83
+ #
84
+ # @example DSL
85
+ # User:
86
+ # @seed 12345
87
+ # id: uuid
88
+ # name: name
89
+ #
90
+ # @example AST
91
+ # SchemaNode.new(
92
+ # name: "User",
93
+ # fields: [FieldNode.new(...), FieldNode.new(...)],
94
+ # behaviors: [BehaviorNode.new(name: :seed, value: 12345)]
95
+ # )
96
+ #
97
+
98
+ class SchemaNode < Node
99
+
100
+ # @!attribute [r] name
101
+ # The schema name (e.g., "User", "Order")
102
+ # @return [String] schema name
103
+
104
+ attr_reader :name
105
+
106
+ # @!attribute [r] fields
107
+ # Field definitions within this schema
108
+ # @return [Array<FieldNode>] list of field nodes
109
+
110
+ attr_reader :fields
111
+
112
+ # @!attribute [r] behaviors
113
+ # Schema-level behaviors (@latency, @seed, etc.)
114
+ # @return [Array<BehaviorNode>] list of behavior nodes
115
+
116
+ attr_reader :behaviors
117
+
118
+ # @!attribute [r] parent_name
119
+ # Name of the schema this one inherits from (nil if none)
120
+ # @return [String, nil] parent schema name
121
+
122
+ attr_reader :parent_name
123
+
124
+ # Create a new SchemaNode
125
+ #
126
+ # @param name [String] the schema name
127
+ # @param fields [Array<FieldNode>] field definitions
128
+ # @param behaviors [Array<BehaviorNode>] schema-level behaviors
129
+ # @param parent_name [String, nil] name of an inherited parent schema
130
+ # @param line [Integer, nil] source line number
131
+ #
132
+
133
+ def initialize(name:, fields: [], behaviors: [], parent_name: nil, line: nil)
134
+ super(line: line)
135
+ @name = name
136
+ @fields = fields
137
+ @behaviors = behaviors
138
+ @parent_name = parent_name
139
+ end
140
+
141
+ # Convert this AST node to a Schema object
142
+ #
143
+ # Creates a Synthra::Schema instance from this node,
144
+ # converting all child nodes (fields, behaviors) as well.
145
+ #
146
+ # @return [Synthra::Schema] the schema object
147
+ #
148
+ # @example
149
+ # schema_node.to_schema
150
+ # # => #<Schema User fields=2 behaviors=1>
151
+ #
152
+
153
+ def to_schema
154
+ all_behaviors = behaviors.map(&:to_behavior)
155
+
156
+ # Extract version from behaviors
157
+ version_behavior = all_behaviors.find { |b| b[:name] == :version }
158
+ version = version_behavior&.dig(:value)
159
+
160
+ # Extract deprecated from behaviors
161
+ deprecated_behavior = all_behaviors.find { |b| b[:name] == :deprecated }
162
+ deprecated = !deprecated_behavior.nil?
163
+ deprecation_message = deprecated_behavior&.dig(:value)
164
+
165
+ # Filter out version/deprecated from runtime behaviors
166
+ runtime_behaviors = all_behaviors.reject { |b| [:version, :deprecated].include?(b[:name]) }
167
+
168
+ Schema.new(
169
+ name: name,
170
+ fields: fields.map(&:to_field),
171
+ behaviors: runtime_behaviors,
172
+ metadata: {},
173
+ version: version,
174
+ deprecated: deprecated,
175
+ deprecation_message: deprecation_message
176
+ )
177
+ end
178
+ end
179
+
180
+ # Field definition node
181
+ #
182
+ # Represents a single field within a schema. Contains the field
183
+ # name, type information, optional flag, conditions, and behaviors.
184
+ #
185
+ # @example DSL
186
+ # email?: email if has_contact @partial_data 20%
187
+ #
188
+ # @example AST
189
+ # FieldNode.new(
190
+ # name: "email",
191
+ # type_node: TypeNode.new(name: "email"),
192
+ # optional: true,
193
+ # condition: ConditionNode.new(field_name: "has_contact"),
194
+ # behaviors: [BehaviorNode.new(name: :partial_data, value: 20)]
195
+ # )
196
+ #
197
+
198
+ class FieldNode < Node
199
+
200
+ # @!attribute [r] name
201
+ # The field name (without ? suffix)
202
+ # @return [String] field name
203
+
204
+ attr_reader :name
205
+
206
+ # @!attribute [r] type_node
207
+ # The type definition for this field
208
+ # @return [TypeNode] type node
209
+
210
+ attr_reader :type_node
211
+
212
+ # @!attribute [r] optional
213
+ # Whether the field is optional (? suffix)
214
+ # @return [Boolean] true if optional
215
+
216
+ attr_reader :optional
217
+
218
+ # @!attribute [r] condition
219
+ # Condition for including this field
220
+ # @return [ConditionNode, nil] condition or nil
221
+
222
+ attr_reader :condition
223
+
224
+ # @!attribute [r] behaviors
225
+ # Field-level behaviors
226
+ # @return [Array<BehaviorNode>] list of behaviors
227
+
228
+ attr_reader :behaviors
229
+
230
+ # Create a new FieldNode
231
+ #
232
+ # @param name [String] field name (without ?)
233
+ # @param type_node [TypeNode] type definition
234
+ # @param optional [Boolean] is field optional?
235
+ # @param condition [ConditionNode, nil] inclusion condition
236
+ # @param behaviors [Array<BehaviorNode>] field behaviors
237
+ # @param line [Integer, nil] source line number
238
+ #
239
+
240
+ def initialize(name:, type_node:, optional: false, condition: nil, behaviors: [], line: nil)
241
+ super(line: line)
242
+ @name = name
243
+ @type_node = type_node
244
+ @optional = optional
245
+ @condition = condition
246
+ @behaviors = behaviors
247
+ end
248
+
249
+ # Check if this field is optional
250
+ #
251
+ # @return [Boolean] true if the field is optional
252
+ #
253
+
254
+ def optional?
255
+ @optional
256
+ end
257
+
258
+ # Convert this AST node to a Field object
259
+ #
260
+ # Creates a Synthra::Field instance from this node.
261
+ #
262
+ # @return [Synthra::Field] the field object
263
+ #
264
+ # @example
265
+ # field_node.to_field
266
+ # # => #<Field email?: email>
267
+ #
268
+
269
+ def to_field
270
+ Field.new(
271
+ name: optional? ? "#{name}?" : name,
272
+ type_name: type_node.name,
273
+ type_args: type_node.arguments,
274
+ nullable: type_node.nullable?,
275
+ condition: condition&.field_name,
276
+ behaviors: behaviors.map(&:to_field_behavior)
277
+ )
278
+ end
279
+ end
280
+
281
+ # Type reference node
282
+ #
283
+ # Represents a type declaration with optional arguments. This is the
284
+ # base class for type nodes; ArrayTypeNode and CustomNode extend it
285
+ # for special cases.
286
+ #
287
+ # @example DSL
288
+ # age: number(18..80)
289
+ # status: enum(active, inactive:30%)
290
+ #
291
+ # @example AST
292
+ # TypeNode.new(
293
+ # name: "number",
294
+ # arguments: { range: 18..80 }
295
+ # )
296
+ #
297
+
298
+ class TypeNode < Node
299
+
300
+ # @!attribute [r] name
301
+ # The type name (uuid, text, number, etc.)
302
+ # @return [String] type name
303
+
304
+ attr_reader :name
305
+
306
+ # @!attribute [r] arguments
307
+ # Type arguments (min, max, values, etc.)
308
+ # @return [Hash] argument name => value pairs
309
+
310
+ attr_reader :arguments
311
+
312
+ # @!attribute [r] nullable
313
+ # Whether the type can be null (? suffix on type)
314
+ # @return [Boolean] true if nullable
315
+
316
+ attr_reader :nullable
317
+
318
+ # Create a new TypeNode
319
+ #
320
+ # @param name [String] type name (may have ? suffix)
321
+ # @param arguments [Hash] type arguments
322
+ # @param nullable [Boolean] is type nullable?
323
+ # @param line [Integer, nil] source line number
324
+ #
325
+
326
+ def initialize(name:, arguments: {}, nullable: false, line: nil)
327
+ super(line: line)
328
+ @name = name.to_s.delete_suffix("?")
329
+ @nullable = nullable || name.to_s.end_with?("?")
330
+ @arguments = arguments
331
+ end
332
+
333
+ # Check if this type can be null
334
+ #
335
+ # @return [Boolean] true if nullable
336
+ #
337
+
338
+ def nullable?
339
+ @nullable
340
+ end
341
+ end
342
+
343
+ # Behavior annotation node
344
+ #
345
+ # Represents a behavior declaration at schema or field level.
346
+ # Behaviors modify generation (latency, failure) or set metadata (seed).
347
+ #
348
+ # @example DSL
349
+ # @latency 100..500ms
350
+ # @failure 10%
351
+ # @seed 12345
352
+ #
353
+ # @example AST
354
+ # BehaviorNode.new(name: :latency, value: 100..500)
355
+ # BehaviorNode.new(name: :failure, value: 10)
356
+ # BehaviorNode.new(name: :seed, value: 12345)
357
+ #
358
+
359
+ class BehaviorNode < Node
360
+
361
+ # @!attribute [r] name
362
+ # Behavior name (latency, failure, seed, etc.)
363
+ # @return [Symbol] behavior name
364
+
365
+ attr_reader :name
366
+
367
+ # @!attribute [r] value
368
+ # Behavior configuration value
369
+ # @return [Object] the value (Integer, Range, Hash, etc.)
370
+
371
+ attr_reader :value
372
+
373
+ # Create a new BehaviorNode
374
+ #
375
+ # @param name [String, Symbol] behavior name
376
+ # @param value [Object] behavior value
377
+ # @param line [Integer, nil] source line number
378
+ #
379
+
380
+ def initialize(name:, value:, line: nil)
381
+ super(line: line)
382
+ @name = name.to_sym
383
+ @value = value
384
+ end
385
+
386
+ # Convert to FieldBehavior for field-level behaviors
387
+ #
388
+ # @return [Synthra::FieldBehavior] field behavior object
389
+ #
390
+
391
+ def to_field_behavior
392
+ FieldBehavior.new(type: name, value: value)
393
+ end
394
+
395
+ # Convert to behavior configuration hash for schema-level
396
+ #
397
+ # @return [Hash] behavior configuration hash
398
+ #
399
+
400
+ def to_behavior
401
+ { name: name, value: value }
402
+ end
403
+ end
404
+
405
+ # Condition node for conditional fields
406
+ #
407
+ # Represents the condition part of a conditional field (if ...).
408
+ # The condition references another field that must be truthy.
409
+ #
410
+ # @example DSL
411
+ # admin_note: text if is_admin
412
+ #
413
+ # @example AST
414
+ # ConditionNode.new(field_name: "is_admin")
415
+ #
416
+
417
+ class ConditionNode < Node
418
+
419
+ # Dangerous method names that should never be allowed in conditions
420
+ # @api private
421
+ DANGEROUS_METHODS = %w[class instance_eval eval send __send__ method_missing].freeze
422
+
423
+ # @!attribute [r] field_name
424
+ # Name of the field to check
425
+ # @return [String] field name
426
+
427
+ attr_reader :field_name
428
+
429
+ # Create a new ConditionNode
430
+ #
431
+ # @param field_name [String] name of field to check for truthiness
432
+ # @param line [Integer, nil] source line number
433
+ #
434
+
435
+ def initialize(field_name:, line: nil)
436
+ super(line: line)
437
+ @field_name = field_name
438
+ end
439
+
440
+ # Evaluate the condition against a context
441
+ #
442
+ # Checks if the referenced field has a truthy value in the
443
+ # given context. Security: Only uses hash access, never method calls,
444
+ # and validates field names against a whitelist of dangerous methods.
445
+ #
446
+ # @param context [Hash, Generator::Context] generation context
447
+ # @return [Boolean] true if condition is met
448
+ #
449
+ # @example
450
+ # condition = ConditionNode.new(field_name: "is_admin")
451
+ # condition.evaluate({ "is_admin" => true }) # => true
452
+ # condition.evaluate({ "is_admin" => false }) # => false
453
+ # condition.evaluate({ "is_admin" => nil }) # => false
454
+ #
455
+
456
+ def evaluate(context)
457
+
458
+ # Security: Only use hash access, never public_send
459
+ # Reject dangerous field names that could be exploited
460
+ field_name_str = @field_name.to_s
461
+ return false if field_name_str.start_with?("__")
462
+
463
+ # Whitelist validation: reject dangerous method names
464
+ return false if self.class::DANGEROUS_METHODS.include?(field_name_str)
465
+
466
+ return false unless context.respond_to?(:[])
467
+
468
+ value = context[@field_name] || context[@field_name.to_sym]
469
+ !!value
470
+ end
471
+ end
472
+
473
+ # Reference to another schema's field
474
+ #
475
+ # Represents a Ref() expression that references a field from
476
+ # another schema. Used for cross-schema relationships.
477
+ #
478
+ # @example DSL
479
+ # user_id: Ref(User.id)
480
+ # category: Ref(Category.name)
481
+ #
482
+ # @example AST
483
+ # ReferenceNode.new(
484
+ # schema_name: "User",
485
+ # field_path: "id"
486
+ # )
487
+ #
488
+
489
+ class ReferenceNode < Node
490
+
491
+ # @!attribute [r] schema_name
492
+ # Name of the referenced schema
493
+ # @return [String] schema name
494
+
495
+ attr_reader :schema_name
496
+
497
+ # @!attribute [r] field_path
498
+ # Path to the field within the schema
499
+ # @return [String] field path (may contain dots for nesting)
500
+
501
+ attr_reader :field_path
502
+
503
+ # Create a new ReferenceNode
504
+ #
505
+ # @param schema_name [String] name of referenced schema
506
+ # @param field_path [String] path to field in schema
507
+ # @param line [Integer, nil] source line number
508
+ #
509
+
510
+ def initialize(schema_name:, field_path:, line: nil)
511
+ super(line: line)
512
+ @schema_name = schema_name
513
+ @field_path = field_path
514
+ end
515
+
516
+ # Get full reference string
517
+ #
518
+ # @return [String] "SchemaName.field_path"
519
+ #
520
+ # @example
521
+ # ref.to_s # => "User.id"
522
+ #
523
+
524
+ def to_s
525
+ "#{schema_name}.#{field_path}"
526
+ end
527
+ end
528
+
529
+ # Modifier node for field modifiers
530
+ #
531
+ # Represents a modifier applied to a field (unique, range, etc.).
532
+ #
533
+ # @example DSL
534
+ # id: uuid unique
535
+ # age: number(18..80) # range is often embedded in type args
536
+ #
537
+ # @example AST
538
+ # ModifierNode.new(name: :unique, value: true)
539
+ #
540
+
541
+ class ModifierNode < Node
542
+
543
+ # @!attribute [r] name
544
+ # Modifier name (unique, range, etc.)
545
+ # @return [Symbol] modifier name
546
+
547
+ attr_reader :name
548
+
549
+ # @!attribute [r] value
550
+ # Modifier value
551
+ # @return [Object] the value
552
+
553
+ attr_reader :value
554
+
555
+ # Create a new ModifierNode
556
+ #
557
+ # @param name [String, Symbol] modifier name
558
+ # @param value [Object] modifier value
559
+ # @param line [Integer, nil] source line number
560
+ #
561
+
562
+ def initialize(name:, value:, line: nil)
563
+ super(line: line)
564
+ @name = name.to_sym
565
+ @value = value
566
+ end
567
+ end
568
+
569
+ # Enum value with optional probability
570
+ #
571
+ # Represents a single value within an enum type, optionally
572
+ # with a probability weight.
573
+ #
574
+ # @example DSL
575
+ # status: enum(pending, active:60%, completed:30%)
576
+ #
577
+ # @example AST
578
+ # EnumValueNode.new(value: "pending")
579
+ # EnumValueNode.new(value: "active", probability: 60)
580
+ #
581
+
582
+ class EnumValueNode < Node
583
+
584
+ # @!attribute [r] value
585
+ # The enum value
586
+ # @return [String] enum value
587
+
588
+ attr_reader :value
589
+
590
+ # @!attribute [r] probability
591
+ # Optional probability percentage
592
+ # @return [Integer, nil] probability (0-100) or nil
593
+
594
+ attr_reader :probability
595
+
596
+ # Create a new EnumValueNode
597
+ #
598
+ # @param value [String] enum value
599
+ # @param probability [Integer, nil] probability percentage
600
+ # @param line [Integer, nil] source line number
601
+ #
602
+
603
+ def initialize(value:, probability: nil, line: nil)
604
+ super(line: line)
605
+ @value = value
606
+ @probability = probability
607
+ end
608
+ end
609
+
610
+ # Array type node
611
+ #
612
+ # Represents an array type with element type and size specification.
613
+ # Extends TypeNode with array-specific attributes.
614
+ #
615
+ # @example DSL
616
+ # tags: array(text, 1..5)
617
+ # items: array(Item, 3)
618
+ #
619
+ # @example AST
620
+ # ArrayTypeNode.new(
621
+ # element_type: TypeNode.new(name: "text"),
622
+ # size_range: 1..5
623
+ # )
624
+ #
625
+
626
+ class ArrayTypeNode < TypeNode
627
+
628
+ # @!attribute [r] element_type
629
+ # Type of elements in the array
630
+ # @return [TypeNode] element type node
631
+
632
+ attr_reader :element_type
633
+
634
+ # @!attribute [r] size_range
635
+ # Size specification (Range or fixed Integer)
636
+ # @return [Range, Integer, Hash] size specification
637
+
638
+ attr_reader :size_range
639
+
640
+ # Create a new ArrayTypeNode
641
+ #
642
+ # @param element_type [TypeNode] element type
643
+ # @param size_range [Range, Integer, Hash] size specification
644
+ # @param nullable [Boolean] whether the array can be null
645
+ # @param line [Integer, nil] source line number
646
+ #
647
+
648
+ def initialize(element_type:, size_range:, nullable: false, line: nil)
649
+ super(
650
+ name: "array",
651
+ arguments: { element: element_type, size: size_range },
652
+ nullable: nullable,
653
+ line: line
654
+ )
655
+ @element_type = element_type
656
+ @size_range = size_range
657
+ end
658
+ end
659
+
660
+ # Custom function call node
661
+ #
662
+ # Represents a custom() type that calls a registered function.
663
+ # The function is called during generation with the context.
664
+ #
665
+ # @example DSL
666
+ # full_name: custom(:compute_full_name)
667
+ #
668
+ # @example AST
669
+ # CustomNode.new(function_name: :compute_full_name)
670
+ #
671
+
672
+ class CustomNode < TypeNode
673
+
674
+ # @!attribute [r] function_name
675
+ # Name of the function to call
676
+ # @return [Symbol] function name
677
+
678
+ attr_reader :function_name
679
+
680
+ # Create a new CustomNode
681
+ #
682
+ # @param function_name [Symbol] name of registered function
683
+ # @param line [Integer, nil] source line number
684
+ #
685
+
686
+ def initialize(function_name:, line: nil)
687
+ super(
688
+ name: "custom",
689
+ arguments: { function: function_name },
690
+ line: line
691
+ )
692
+ @function_name = function_name
693
+ end
694
+ end
695
+
696
+ # =========================================================================
697
+ # Simulyra DSL - New AST Nodes for Layered Architecture
698
+ # =========================================================================
699
+
700
+ # Program node - root of the AST
701
+ #
702
+ # Contains all top-level definitions: schemas and APIs.
703
+ # This is the entry point for the parsed DSL.
704
+ #
705
+ # @example DSL
706
+ # schema User:
707
+ # id: uuid
708
+ #
709
+ # api GET /users:
710
+ # scenario Success:
711
+ # @status 200
712
+ #
713
+ # @example AST
714
+ # ProgramNode.new(
715
+ # schemas: [SchemaDefinitionNode.new(...)],
716
+ # apis: [ApiDefinitionNode.new(...)]
717
+ # )
718
+ #
719
+
720
+ class ProgramNode < Node
721
+
722
+ # @!attribute [r] schemas
723
+ # Schema definitions in the program
724
+ # @return [Array<SchemaDefinitionNode>] list of schema definitions
725
+
726
+ attr_reader :schemas
727
+
728
+ # @!attribute [r] apis
729
+ # API definitions in the program
730
+ # @return [Array<ApiDefinitionNode>] list of API definitions
731
+
732
+ attr_reader :apis
733
+
734
+ # @!attribute [r] legacy_schemas
735
+ # Legacy schema definitions (without 'schema' keyword)
736
+ # @return [Array<SchemaNode>] list of legacy schema nodes
737
+
738
+ attr_reader :legacy_schemas
739
+
740
+ # Create a new ProgramNode
741
+ #
742
+ # @param schemas [Array<SchemaDefinitionNode>] schema definitions
743
+ # @param apis [Array<ApiDefinitionNode>] API definitions
744
+ # @param legacy_schemas [Array<SchemaNode>] legacy schema definitions
745
+ # @param line [Integer, nil] source line number
746
+ #
747
+
748
+ def initialize(schemas: [], apis: [], legacy_schemas: [], line: nil)
749
+ super(line: line)
750
+ @schemas = schemas
751
+ @apis = apis
752
+ @legacy_schemas = legacy_schemas
753
+ end
754
+
755
+ # Get all schema nodes (both new and legacy)
756
+ #
757
+ # @return [Array<SchemaDefinitionNode, SchemaNode>] all schemas
758
+ #
759
+
760
+ def all_schemas
761
+ schemas + legacy_schemas
762
+ end
763
+ end
764
+
765
+ # Schema definition node (new syntax)
766
+ #
767
+ # Represents a schema definition using the new Simulyra syntax:
768
+ # `schema Name:`
769
+ #
770
+ # This differs from the legacy SchemaNode which just uses `Name:`
771
+ #
772
+ # @example DSL
773
+ # schema User:
774
+ # id: uuid
775
+ # email: email
776
+ #
777
+ # @example AST
778
+ # SchemaDefinitionNode.new(
779
+ # name: "User",
780
+ # fields: [FieldNode.new(...), FieldNode.new(...)]
781
+ # )
782
+ #
783
+
784
+ class SchemaDefinitionNode < Node
785
+
786
+ # @!attribute [r] name
787
+ # The schema name
788
+ # @return [String] schema name
789
+
790
+ attr_reader :name
791
+
792
+ # @!attribute [r] fields
793
+ # Field definitions within this schema
794
+ # @return [Array<FieldNode>] list of field nodes
795
+
796
+ attr_reader :fields
797
+
798
+ # @!attribute [r] behaviors
799
+ # Schema-level behaviors (for compatibility)
800
+ # @return [Array<BehaviorNode>] list of behavior nodes
801
+
802
+ attr_reader :behaviors
803
+
804
+ # Create a new SchemaDefinitionNode
805
+ #
806
+ # @param name [String] the schema name
807
+ # @param fields [Array<FieldNode>] field definitions
808
+ # @param behaviors [Array<BehaviorNode>] schema-level behaviors
809
+ # @param line [Integer, nil] source line number
810
+ #
811
+
812
+ def initialize(name:, fields: [], behaviors: [], line: nil)
813
+ super(line: line)
814
+ @name = name
815
+ @fields = fields
816
+ @behaviors = behaviors
817
+ end
818
+
819
+ # Convert to Schema object
820
+ #
821
+ # @return [Synthra::Schema] the schema object
822
+ #
823
+
824
+ def to_schema
825
+ all_behaviors = behaviors.map(&:to_behavior)
826
+
827
+ version_behavior = all_behaviors.find { |b| b[:name] == :version }
828
+ version = version_behavior&.dig(:value)
829
+
830
+ deprecated_behavior = all_behaviors.find { |b| b[:name] == :deprecated }
831
+ deprecated = !deprecated_behavior.nil?
832
+ deprecation_message = deprecated_behavior&.dig(:value)
833
+
834
+ runtime_behaviors = all_behaviors.reject { |b| [:version, :deprecated].include?(b[:name]) }
835
+
836
+ Schema.new(
837
+ name: name,
838
+ fields: fields.map(&:to_field),
839
+ behaviors: runtime_behaviors,
840
+ metadata: {},
841
+ version: version,
842
+ deprecated: deprecated,
843
+ deprecation_message: deprecation_message
844
+ )
845
+ end
846
+ end
847
+
848
+ # API definition node
849
+ #
850
+ # Represents an API endpoint definition with HTTP method, path,
851
+ # and response scenarios.
852
+ #
853
+ # @example DSL
854
+ # api GET /users:
855
+ # scenario Success:
856
+ # @status 200
857
+ # users: schema(:User).array
858
+ #
859
+ # @example AST
860
+ # ApiDefinitionNode.new(
861
+ # method: :get,
862
+ # path: "/users",
863
+ # scenarios: [ScenarioNode.new(...)]
864
+ # )
865
+ #
866
+
867
+ class ApiDefinitionNode < Node
868
+
869
+ # @!attribute [r] method
870
+ # HTTP method (get, post, put, patch, delete, options, head)
871
+ # @return [Symbol] HTTP method as lowercase symbol
872
+
873
+ attr_reader :method
874
+
875
+ # @!attribute [r] path
876
+ # URL path (e.g., "/users", "/users/:id")
877
+ # @return [String] URL path
878
+
879
+ attr_reader :path
880
+
881
+ # @!attribute [r] scenarios
882
+ # Response scenarios for this API
883
+ # @return [Array<ScenarioNode>] list of scenario nodes
884
+
885
+ attr_reader :scenarios
886
+
887
+ # Create a new ApiDefinitionNode
888
+ #
889
+ # @param method [Symbol] HTTP method (:get, :post, etc.)
890
+ # @param path [String] URL path
891
+ # @param scenarios [Array<ScenarioNode>] response scenarios
892
+ # @param line [Integer, nil] source line number
893
+ #
894
+
895
+ def initialize(method:, path:, scenarios: [], line: nil)
896
+ super(line: line)
897
+ @method = method
898
+ @path = path
899
+ @scenarios = scenarios
900
+ end
901
+ end
902
+
903
+ # Scenario node
904
+ #
905
+ # Represents a response scenario within an API definition.
906
+ # Scenarios define possible HTTP responses with status codes,
907
+ # probabilities, and response body shapes.
908
+ #
909
+ # @example DSL
910
+ # scenario Success:
911
+ # @status 200
912
+ # @probability 80%
913
+ # users: schema(:User).array
914
+ #
915
+ # @example AST
916
+ # ScenarioNode.new(
917
+ # name: "Success",
918
+ # directives: [StatusDirective.new(status_code: 200), ...],
919
+ # response_fields: [ResponseFieldNode.new(...)]
920
+ # )
921
+ #
922
+
923
+ class ScenarioNode < Node
924
+
925
+ # @!attribute [r] name
926
+ # Scenario name (e.g., "Success", "NotFound", "ValidationError")
927
+ # @return [String] scenario name
928
+
929
+ attr_reader :name
930
+
931
+ # @!attribute [r] directives
932
+ # HTTP directives (@status, @probability, @header, @latency)
933
+ # @return [Array<DirectiveNode>] list of directive nodes
934
+
935
+ attr_reader :directives
936
+
937
+ # @!attribute [r] response_fields
938
+ # Response body field definitions
939
+ # @return [Array<ResponseFieldNode>] list of response field nodes
940
+
941
+ attr_reader :response_fields
942
+
943
+ # Create a new ScenarioNode
944
+ #
945
+ # @param name [String] scenario name
946
+ # @param directives [Array<DirectiveNode>] HTTP directives
947
+ # @param response_fields [Array<ResponseFieldNode>] response fields
948
+ # @param line [Integer, nil] source line number
949
+ #
950
+
951
+ def initialize(name:, directives: [], response_fields: [], line: nil)
952
+ super(line: line)
953
+ @name = name
954
+ @directives = directives
955
+ @response_fields = response_fields
956
+ end
957
+
958
+ # Get the status code from directives
959
+ #
960
+ # @return [Integer, nil] HTTP status code or nil
961
+ #
962
+
963
+ def status_code
964
+ status_directive = directives.find { |d| d.is_a?(StatusDirective) }
965
+ status_directive&.status_code
966
+ end
967
+
968
+ # Get the probability from directives
969
+ #
970
+ # @return [Integer, nil] probability percentage or nil
971
+ #
972
+
973
+ def probability
974
+ prob_directive = directives.find { |d| d.is_a?(ProbabilityDirective) }
975
+ prob_directive&.percentage
976
+ end
977
+ end
978
+
979
+ # Response field node
980
+ #
981
+ # Represents a field in a scenario's response body.
982
+ # Similar to FieldNode but specifically for response definitions.
983
+ #
984
+ # @example DSL
985
+ # users: schema(:User).array
986
+ # message: string
987
+ #
988
+ # @example AST
989
+ # ResponseFieldNode.new(
990
+ # name: "users",
991
+ # type_node: SchemaReferenceNode.new(schema_name: "User", array: true)
992
+ # )
993
+ #
994
+
995
+ class ResponseFieldNode < Node
996
+
997
+ # @!attribute [r] name
998
+ # Field name
999
+ # @return [String] field name
1000
+
1001
+ attr_reader :name
1002
+
1003
+ # @!attribute [r] type_node
1004
+ # Type definition
1005
+ # @return [TypeNode, SchemaReferenceNode] type node
1006
+
1007
+ attr_reader :type_node
1008
+
1009
+ # Create a new ResponseFieldNode
1010
+ #
1011
+ # @param name [String] field name
1012
+ # @param type_node [TypeNode] type definition
1013
+ # @param line [Integer, nil] source line number
1014
+ #
1015
+
1016
+ def initialize(name:, type_node:, line: nil)
1017
+ super(line: line)
1018
+ @name = name
1019
+ @type_node = type_node
1020
+ end
1021
+ end
1022
+
1023
+ # Schema reference node
1024
+ #
1025
+ # Represents an explicit schema reference using the syntax:
1026
+ # `schema(:Name)` or `schema(:Name).array`
1027
+ #
1028
+ # This is the ONLY valid way to reference schemas in the new DSL.
1029
+ # The system must NEVER infer schemas from capitalization.
1030
+ #
1031
+ # @example DSL
1032
+ # user: schema(:User)
1033
+ # users: schema(:User).array
1034
+ # users: schema(:User).array(1..10)
1035
+ #
1036
+ # @example AST
1037
+ # SchemaReferenceNode.new(schema_name: "User")
1038
+ # SchemaReferenceNode.new(schema_name: "User", array: true)
1039
+ # SchemaReferenceNode.new(schema_name: "User", array: true, array_range: 1..10)
1040
+ #
1041
+
1042
+ class SchemaReferenceNode < TypeNode
1043
+
1044
+ # @!attribute [r] schema_name
1045
+ # Name of the referenced schema
1046
+ # @return [String] schema name
1047
+
1048
+ attr_reader :schema_name
1049
+
1050
+ # @!attribute [r] array
1051
+ # Whether this is an array of the schema type
1052
+ # @return [Boolean] true if array
1053
+
1054
+ attr_reader :array
1055
+
1056
+ # @!attribute [r] array_range
1057
+ # Optional range for array size
1058
+ # @return [Range, nil] size range or nil
1059
+
1060
+ attr_reader :array_range
1061
+
1062
+ # @!attribute [r] optional
1063
+ # Whether this reference is optional
1064
+ # @return [Boolean] true if optional
1065
+
1066
+ attr_reader :optional
1067
+
1068
+ # Create a new SchemaReferenceNode
1069
+ #
1070
+ # @param schema_name [String] name of the referenced schema
1071
+ # @param array [Boolean] whether this is an array
1072
+ # @param array_range [Range, nil] optional array size range
1073
+ # @param optional [Boolean] whether the reference is optional
1074
+ # @param line [Integer, nil] source line number
1075
+ #
1076
+
1077
+ def initialize(schema_name:, array: false, array_range: nil, optional: false, line: nil)
1078
+ super(
1079
+ name: "schema_ref",
1080
+ arguments: {
1081
+ schema_name: schema_name,
1082
+ array: array,
1083
+ array_range: array_range
1084
+ },
1085
+ nullable: optional,
1086
+ line: line
1087
+ )
1088
+ @schema_name = schema_name
1089
+ @array = array
1090
+ @array_range = array_range
1091
+ @optional = optional
1092
+ end
1093
+
1094
+ # Check if this is an array reference
1095
+ #
1096
+ # @return [Boolean] true if array
1097
+ #
1098
+
1099
+ def array?
1100
+ @array
1101
+ end
1102
+
1103
+ # Check if this is an optional reference
1104
+ #
1105
+ # @return [Boolean] true if optional
1106
+ #
1107
+
1108
+ def optional?
1109
+ @optional
1110
+ end
1111
+ end
1112
+
1113
+ # =========================================================================
1114
+ # Directive Nodes for Scenarios
1115
+ # =========================================================================
1116
+
1117
+ # Base class for directive nodes
1118
+ #
1119
+ # Directives are scenario-level declarations that control HTTP behavior.
1120
+ # All directives start with @ in the DSL.
1121
+ #
1122
+
1123
+ class DirectiveNode < Node
1124
+ end
1125
+
1126
+ # Status directive
1127
+ #
1128
+ # Sets the HTTP status code for a scenario.
1129
+ #
1130
+ # @example DSL
1131
+ # @status 200
1132
+ # @status 404
1133
+ #
1134
+
1135
+ class StatusDirective < DirectiveNode
1136
+
1137
+ # @!attribute [r] status_code
1138
+ # HTTP status code
1139
+ # @return [Integer] status code (100-599)
1140
+
1141
+ attr_reader :status_code
1142
+
1143
+ # Create a new StatusDirective
1144
+ #
1145
+ # @param status_code [Integer] HTTP status code
1146
+ # @param line [Integer, nil] source line number
1147
+ #
1148
+
1149
+ def initialize(status_code:, line: nil)
1150
+ super(line: line)
1151
+ @status_code = status_code
1152
+ end
1153
+ end
1154
+
1155
+ # Probability directive
1156
+ #
1157
+ # Sets the probability of this scenario being selected.
1158
+ #
1159
+ # @example DSL
1160
+ # @probability 80%
1161
+ #
1162
+
1163
+ class ProbabilityDirective < DirectiveNode
1164
+
1165
+ # @!attribute [r] percentage
1166
+ # Probability percentage (0-100)
1167
+ # @return [Integer] percentage
1168
+
1169
+ attr_reader :percentage
1170
+
1171
+ # Create a new ProbabilityDirective
1172
+ #
1173
+ # @param percentage [Integer] probability percentage
1174
+ # @param line [Integer, nil] source line number
1175
+ #
1176
+
1177
+ def initialize(percentage:, line: nil)
1178
+ super(line: line)
1179
+ @percentage = percentage
1180
+ end
1181
+ end
1182
+
1183
+ # Latency directive
1184
+ #
1185
+ # Sets simulated response latency for a scenario.
1186
+ #
1187
+ # @example DSL
1188
+ # @latency 100..500ms
1189
+ #
1190
+
1191
+ class LatencyDirective < DirectiveNode
1192
+
1193
+ # @!attribute [r] min_ms
1194
+ # Minimum latency in milliseconds
1195
+ # @return [Integer] minimum latency
1196
+
1197
+ attr_reader :min_ms
1198
+
1199
+ # @!attribute [r] max_ms
1200
+ # Maximum latency in milliseconds
1201
+ # @return [Integer] maximum latency
1202
+
1203
+ attr_reader :max_ms
1204
+
1205
+ # Create a new LatencyDirective
1206
+ #
1207
+ # @param min_ms [Integer] minimum latency
1208
+ # @param max_ms [Integer] maximum latency
1209
+ # @param line [Integer, nil] source line number
1210
+ #
1211
+
1212
+ def initialize(min_ms:, max_ms:, line: nil)
1213
+ super(line: line)
1214
+ @min_ms = min_ms
1215
+ @max_ms = max_ms
1216
+ end
1217
+ end
1218
+
1219
+ # Header directive
1220
+ #
1221
+ # Sets a custom HTTP header for the response.
1222
+ #
1223
+ # @example DSL
1224
+ # @header Content-Type: application/json
1225
+ # @header X-Custom: value
1226
+ #
1227
+
1228
+ class HeaderDirective < DirectiveNode
1229
+
1230
+ # @!attribute [r] key
1231
+ # Header name
1232
+ # @return [String] header key
1233
+
1234
+ attr_reader :key
1235
+
1236
+ # @!attribute [r] value
1237
+ # Header value
1238
+ # @return [String] header value
1239
+
1240
+ attr_reader :value
1241
+
1242
+ # Create a new HeaderDirective
1243
+ #
1244
+ # @param key [String] header name
1245
+ # @param value [String] header value
1246
+ # @param line [Integer, nil] source line number
1247
+ #
1248
+
1249
+ def initialize(key:, value:, line: nil)
1250
+ super(line: line)
1251
+ @key = key
1252
+ @value = value
1253
+ end
1254
+ end
1255
+
1256
+ # Method directive
1257
+ #
1258
+ # Associates a scenario with specific HTTP methods.
1259
+ # Used in the legacy response scenarios format.
1260
+ #
1261
+ # @example DSL
1262
+ # @method GET
1263
+ # @method POST
1264
+ #
1265
+
1266
+ class MethodDirective < DirectiveNode
1267
+
1268
+ # @!attribute [r] http_method
1269
+ # HTTP method
1270
+ # @return [Symbol] http method as lowercase symbol
1271
+
1272
+ attr_reader :http_method
1273
+
1274
+ # Create a new MethodDirective
1275
+ #
1276
+ # @param http_method [Symbol] HTTP method
1277
+ # @param line [Integer, nil] source line number
1278
+ #
1279
+
1280
+ def initialize(http_method:, line: nil)
1281
+ super(line: line)
1282
+ @http_method = http_method
1283
+ end
1284
+ end
1285
+ end
1286
+ end
1287
+ end