class Lutaml::UmlRepository::ErrorHandler

# => Raises error with suggestions for similar packages
handler.package_not_found_error(“ModelRoot::i-UR::urf”)
@example Package not found
# => Raises error with suggestion: Did you mean “ModelRoot::Building”?
handler.class_not_found_error(“ModelRoot::Buildng”)
handler = ErrorHandler.new(repository)
@example Class not found
suggest similar names that might be what the user intended.
elements are not found. Uses fuzzy matching (Levenshtein distance) to
Provides helpful error messages when classes, packages, or other model
Error handler for user-friendly error messages with suggestions.

def class_not_found_error(attempted_qname)

Raises:
  • (NameError) - With helpful message and suggestions

Parameters:
  • attempted_qname (String) -- The qualified name that was attempted
def class_not_found_error(attempted_qname)
  suggestions = suggest_similar_classes(attempted_qname)
  message = "Class not found: #{attempted_qname}"
  if suggestions.any?
    message += "\n\nDid you mean one of these?"
    suggestions.each { |s| message += "\n  - #{s}" }
  else
    message += "\n\nTip: Use the 'search' or 'find' commands to " \
               "explore available classes."
  end
  raise NameError, message
end

def find_similar_names(attempted, candidates) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength

Returns:
  • (Array) - Sorted array of similar names

Parameters:
  • candidates (Array) -- List of candidate names
  • attempted (String) -- The attempted name
def find_similar_names(attempted, candidates) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
  # Calculate distances for all candidates
  distances = candidates.map do |candidate|
    distance = levenshtein_distance(
      attempted.downcase,
      candidate.downcase,
    )
    { name: candidate, distance: distance }
  end
  # Filter by maximum distance and sort
  similar = distances
    .select { |d| d[:distance] <= MAX_SUGGESTION_DISTANCE }
    .sort_by { |d| d[:distance] }
    .take(MAX_SUGGESTIONS)
    .map { |d| d[:name] }
  # If no close matches, try substring matching
  if similar.empty?
    similar = find_substring_matches(attempted, candidates)
  end
  similar
end

def find_substring_matches(attempted, candidates)

Returns:
  • (Array) - Array of matching names

Parameters:
  • candidates (Array) -- List of candidate names
  • attempted (String) -- The attempted name
def find_substring_matches(attempted, candidates)
  attempted_lower = attempted.downcase
  candidates
    .select { |c| c.downcase.include?(attempted_lower) }
    .take(MAX_SUGGESTIONS)
end

def initialize(repository)

Parameters:
  • repository (UmlRepository) -- Repository to use for suggestions
def initialize(repository)
  @repository = repository
end

def levenshtein_distance(str1, str2) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity

Returns:
  • (Integer) - The Levenshtein distance

Parameters:
  • str2 (String) -- Second string
  • str1 (String) -- First string
def levenshtein_distance(str1, str2) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  return str2.length if str1.empty?
  return str1.length if str2.empty?
  # Create a matrix to store distances
  matrix = Array.new(str1.length + 1) do |i|
    Array.new(str2.length + 1) do |j|
      if i.zero?
        j
      else
        (j.zero? ? i : 0)
      end
    end
  end
  # Calculate distances
  (1..str1.length).each do |i|
    (1..str2.length).each do |j|
      cost = str1[i - 1] == str2[j - 1] ? 0 : 1
      matrix[i][j] = [
        matrix[i - 1][j] + 1,      # deletion
        matrix[i][j - 1] + 1,      # insertion
        matrix[i - 1][j - 1] + cost, # substitution
      ].min
    end
  end
  matrix[str1.length][str2.length]
end

def package_not_found_error(attempted_path)

Raises:
  • (NameError) - With helpful message and suggestions

Parameters:
  • attempted_path (String) -- The package path that was attempted
def package_not_found_error(attempted_path)
  suggestions = suggest_similar_packages(attempted_path)
  message = "Package not found: #{attempted_path}"
  if suggestions.any?
    message += "\n\nDid you mean one of these?"
    suggestions.each { |s| message += "\n  - #{s}" }
  else
    message += "\n\nTip: Use the 'list' or 'tree' commands to explore " \
               "available packages."
  end
  raise NameError, message
end

def suggest_similar_classes(attempted)

Returns:
  • (Array) - Array of suggested qualified names, sorted by

Parameters:
  • attempted (String) -- The attempted qualified name
def suggest_similar_classes(attempted)
  return [] unless repository.indexes[:class_to_qname]
  all_qnames = repository.indexes[:class_to_qname].values
  find_similar_names(attempted, all_qnames)
end

def suggest_similar_packages(attempted)

Returns:
  • (Array) - Array of suggested package paths, sorted by

Parameters:
  • attempted (String) -- The attempted package path
def suggest_similar_packages(attempted)
  return [] unless repository.indexes[:package_to_path]
  all_paths = repository.indexes[:package_to_path].values
  find_similar_names(attempted, all_paths)
end