Class: Synthra::Validator::DslValidator

Inherits:
Object
  • Object
show all
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.

Examples:

Validate all rules

validator = DslValidator.new(program_node)
errors = validator.validate  # Returns array of ValidationError

Raise on first error

validator = DslValidator.new(program_node)
validator.validate!  # Raises ValidationError

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

Constructor Details

#initialize(program) ⇒ DslValidator

Returns a new instance of DslValidator.

Parameters:



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_namesvoid (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

Parameters:

  • target (String)

    the name to match

  • candidates (Array<String>)

    possible matches

Returns:

  • (Array<String>)

    similar names



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

Parameters:

  • s1 (String)

    first string

  • s2 (String)

    second string

Returns:

  • (Integer)

    edit distance



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

#validateArray<Errors::ValidationError>

Validate the DSL and return all errors

Returns:



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

Returns:

  • (true)

    if all validations pass

Raises:



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_contextvoid (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_scenariosvoid (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_schemasvoid (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_sumsvoid (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_statusvoid (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_contextvoid (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_referencesvoid (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

Parameters:

  • type_node (Parser::AST::TypeNode)

    the type to validate

  • context (String)

    context for error message



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