Class: Synthra::Validator::DslValidator
- Inherits:
-
Object
- Object
- Synthra::Validator::DslValidator
- Defined in:
- lib/synthra/validator/dsl_validator.rb
Overview
Validates Simulyra DSL AST structure
The DslValidator checks that the DSL follows the strict separation between Schema, API, and Scenario layers.
Constant Summary collapse
- HTTP_DIRECTIVES =
HTTP-related directives that are only valid in scenarios
%w[status probability latency header method].freeze
- VALID_STATUS_CODES =
Valid HTTP status codes
(100..599).freeze
Instance Method Summary collapse
-
#collect_schema_names ⇒ void
private
Collect all schema names for reference validation.
-
#find_similar_names(target, candidates, max_results: 3) ⇒ Array<String>
private
Find similar names using basic string distance.
-
#initialize(program) ⇒ DslValidator
constructor
A new instance of DslValidator.
-
#levenshtein_distance(s1, s2) ⇒ Integer
private
Simple Levenshtein distance implementation.
-
#validate ⇒ Array<Errors::ValidationError>
Validate the DSL and return all errors.
-
#validate! ⇒ true
Validate and raise on first error.
-
#validate_api_context ⇒ void
private
Validate API context (scenarios should be properly nested).
-
#validate_apis_have_scenarios ⇒ void
private
Validate that APIs have at least one scenario.
-
#validate_no_duplicate_schemas ⇒ void
private
Validate no duplicate schema names.
-
#validate_probability_sums ⇒ void
private
Validate that probability sums don't exceed 100%.
-
#validate_scenarios_have_status ⇒ void
private
Validate that scenarios have @status directive.
-
#validate_schema_context ⇒ void
private
Validate schema context (no HTTP directives in schemas).
-
#validate_schema_references ⇒ void
private
Validate that all schema(:Name) references resolve.
-
#validate_type_references(type_node, context) ⇒ void
private
Validate type node for schema references.
Constructor Details
#initialize(program) ⇒ DslValidator
Returns a new instance of DslValidator.
51 52 53 54 55 |
# File 'lib/synthra/validator/dsl_validator.rb', line 51 def initialize(program) @program = program @errors = [] @schema_names = Set.new end |
Instance Method Details
#collect_schema_names ⇒ void (private)
This method returns an undefined value.
Collect all schema names for reference validation
97 98 99 100 101 102 103 104 105 106 |
# File 'lib/synthra/validator/dsl_validator.rb', line 97 def collect_schema_names @program.schemas.each do |schema| @schema_names.add(schema.name) end # Also collect legacy schema names @program.legacy_schemas.each do |schema| @schema_names.add(schema.name) end end |
#find_similar_names(target, candidates, max_results: 3) ⇒ Array<String> (private)
Find similar names using basic string distance
296 297 298 299 300 301 302 303 304 305 |
# File 'lib/synthra/validator/dsl_validator.rb', line 296 def find_similar_names(target, candidates, max_results: 3) scored = candidates.map do |name| [name, levenshtein_distance(target.downcase, name.downcase)] end scored.select { |_, dist| dist <= 3 } .sort_by { |_, dist| dist } .first(max_results) .map(&:first) end |
#levenshtein_distance(s1, s2) ⇒ Integer (private)
Simple Levenshtein distance implementation
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/synthra/validator/dsl_validator.rb', line 313 def levenshtein_distance(s1, s2) m = s1.length n = s2.length return n if m.zero? return m if n.zero? d = Array.new(m + 1) { Array.new(n + 1, 0) } (0..m).each { |i| d[i][0] = i } (0..n).each { |j| d[0][j] = j } (1..m).each do |i| (1..n).each do |j| cost = s1[i - 1] == s2[j - 1] ? 0 : 1 d[i][j] = [ d[i - 1][j] + 1, # deletion d[i][j - 1] + 1, # insertion d[i - 1][j - 1] + cost # substitution ].min end end d[m][n] end |
#validate ⇒ Array<Errors::ValidationError>
Validate the DSL and return all errors
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/synthra/validator/dsl_validator.rb', line 61 def validate @errors = [] @schema_names = Set.new # Collect all schema names first (for reference validation) collect_schema_names # Run all validation rules validate_no_duplicate_schemas validate_schema_references validate_apis_have_scenarios validate_scenarios_have_status validate_probability_sums validate_schema_context validate_api_context @errors end |
#validate! ⇒ true
Validate and raise on first error
85 86 87 88 89 |
# File 'lib/synthra/validator/dsl_validator.rb', line 85 def validate! errors = validate raise errors.first if errors.any? true end |
#validate_api_context ⇒ void (private)
This method returns an undefined value.
Validate API context (scenarios should be properly nested)
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/synthra/validator/dsl_validator.rb', line 260 def validate_api_context @program.apis.each do |api| # Check for duplicate scenario names within an API seen_scenarios = {} api.scenarios.each do |scenario| if seen_scenarios.key?(scenario.name) @errors << Errors::ScenarioError.new( "Duplicate scenario name \"#{scenario.name}\" in API #{api.method.upcase} #{api.path}", line: scenario.line, suggestion: "Scenario names must be unique within an API." ) else seen_scenarios[scenario.name] = scenario end end # Check for multiple @status directives in a scenario api.scenarios.each do |scenario| status_count = scenario.directives.count { |d| d.is_a?(Parser::AST::StatusDirective) } if status_count > 1 @errors << Errors::ScenarioError.new( "Multiple @status directives are not allowed", line: scenario.line, suggestion: "First defined in scenario \"#{scenario.name}\". Use only one @status per scenario." ) end end end end |
#validate_apis_have_scenarios ⇒ void (private)
This method returns an undefined value.
Validate that APIs have at least one scenario
177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/synthra/validator/dsl_validator.rb', line 177 def validate_apis_have_scenarios @program.apis.each do |api| if api.scenarios.empty? @errors << Errors::ApiError.new( "API #{api.method.upcase} #{api.path} must define at least one scenario", line: api.line, suggestion: "Add a scenario block with @status directive." ) end end end |
#validate_no_duplicate_schemas ⇒ void (private)
This method returns an undefined value.
Validate no duplicate schema names
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/synthra/validator/dsl_validator.rb', line 112 def validate_no_duplicate_schemas seen = {} all_schemas = @program.schemas + @program.legacy_schemas all_schemas.each do |schema| if seen.key?(schema.name) @errors << Errors::SchemaError.new( "Duplicate schema \"#{schema.name}\"", line: schema.line, suggestion: "Defined again at line #{schema.line}. Schema names must be unique." ) else seen[schema.name] = schema end end end |
#validate_probability_sums ⇒ void (private)
This method returns an undefined value.
Validate that probability sums don't exceed 100%
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/synthra/validator/dsl_validator.rb', line 220 def validate_probability_sums @program.apis.each do |api| total = 0 api.scenarios.each do |scenario| total += scenario.probability if scenario.probability end if total > 100 @errors << Errors::ScenarioError.new( "Total scenario probability exceeds 100%", line: api.line, suggestion: "Current total: #{total}% in API #{api.method.upcase} #{api.path}." ) end end end |
#validate_scenarios_have_status ⇒ void (private)
This method returns an undefined value.
Validate that scenarios have @status directive
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/synthra/validator/dsl_validator.rb', line 193 def validate_scenarios_have_status @program.apis.each do |api| api.scenarios.each do |scenario| unless scenario.status_code @errors << Errors::ScenarioError.new( "Scenario \"#{scenario.name}\" must define @status", line: scenario.line, suggestion: "Required for HTTP simulation. Example: @status 200" ) end # Also validate status code is valid if scenario.status_code && !VALID_STATUS_CODES.include?(scenario.status_code) @errors << Errors::ScenarioError.new( "Invalid status code #{scenario.status_code} in scenario \"#{scenario.name}\"", line: scenario.line, suggestion: "Status code must be between 100 and 599." ) end end end end |
#validate_schema_context ⇒ void (private)
This method returns an undefined value.
Validate schema context (no HTTP directives in schemas)
241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/synthra/validator/dsl_validator.rb', line 241 def validate_schema_context all_schemas = @program.schemas + @program.legacy_schemas all_schemas.each do |schema| schema.behaviors.each do |behavior| if HTTP_DIRECTIVES.include?(behavior.name.to_s) @errors << Errors::ContextError.new( "HTTP directives are not allowed inside schema definitions", line: behavior.line, suggestion: "@#{behavior.name} is an HTTP directive. Use it in scenario blocks only." ) end end end end |
#validate_schema_references ⇒ void (private)
This method returns an undefined value.
Validate that all schema(:Name) references resolve
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/synthra/validator/dsl_validator.rb', line 133 def validate_schema_references # Check schema references in schemas all_schemas = @program.schemas + @program.legacy_schemas all_schemas.each do |schema| schema.fields.each do |field| validate_type_references(field.type_node, schema.name) end end # Check schema references in API scenarios @program.apis.each do |api| api.scenarios.each do |scenario| scenario.response_fields.each do |field| validate_type_references(field.type_node, "API #{api.method.upcase} #{api.path}") end end end end |
#validate_type_references(type_node, context) ⇒ void (private)
This method returns an undefined value.
Validate type node for schema references
158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/synthra/validator/dsl_validator.rb', line 158 def validate_type_references(type_node, context) return unless type_node.is_a?(Parser::AST::SchemaReferenceNode) unless @schema_names.include?(type_node.schema_name) similar = find_similar_names(type_node.schema_name, @schema_names.to_a) suggestion = similar.any? ? "Did you mean: #{similar.join(', ')}?" : nil @errors << Errors::ReferenceError.new( "Schema \"#{type_node.schema_name}\" is not defined", line: type_node.line, suggestion: "#{suggestion} Used in #{context}." ) end end |