import re, sys # Parse Chatwoot routes.rb to extract all API routes # Strategy: walk through the file tracking namespace nesting depth with open('/home/yanghao05/Workspace/chatwoot-reference/config/routes.rb') as f: text = f.read() # We need to track namespace nesting and extract route definitions # Rails routes.rb uses nested blocks like: # namespace :api do # namespace :v1 do # resources :accounts do # resources :conversations do # member { post :assign } # end # end # end # end # Parse the file line by line, tracking nesting routes = [] namespace_stack = [] resource_stack = [] # Each entry: (resource_name, is_singular, id_param_name) current_scope = [] # Track scope/member/collection blocks in_member = False in_collection = False skip_frontend = True # Skip frontend/dashboard routes lines = text.split('\n') i = 0 while i < len(lines): line = lines[i].strip() # Skip comments if line.startswith('#'): i += 1 continue # Skip frontend routes (dashboard, app routes) if skip_frontend and ('dashboard' in line or 'app' in line and '/app' in line): # Need to skip entire frontend block if 'root' in line or '/app' in line or 'dashboard#index' in line: # Skip until we hit the next significant keyword i += 1 continue # Detect namespace declarations ns_match = re.match(r'namespace\s+:(\w+)\s+do', line) if ns_match: namespace_stack.append(ns_match.group(1)) i += 1 continue # Detect scope declarations scope_match = re.match(r'scope\s+[\'":](\w+)[\'":]\s+do', line) if scope_match: namespace_stack.append(scope_match.group(1)) i += 1 continue # Detect resources declarations res_match = re.match(r'resources\s+:(\w+)(?:\s*,\s*(.+))?\s+do', line) if res_match: resource_name = res_match.group(1) options = res_match.group(2) or '' # Check if only certain actions only_match = re.search(r'only:\s*\[([^\]]+)\]', options) resource_stack.append(resource_name) i += 1 continue # Also handle resources without block (one-liner) res_simple = re.match(r'resources\s+:(\w+)(?:\s*,\s*(.+))?(?!\s+do)', line) if res_simple and not res_match: resource_name = res_simple.group(1) options = res_simple.group(2) or '' # Generate standard CRUD routes for this resource only_match = re.search(r'only:\s*\[([^\]]+)\]', options) except_match = re.search(r'except:\s*\[([^\]]+)\]', options) allowed_actions = ['index', 'show', 'create', 'update', 'destroy', 'new', 'edit'] if only_match: allowed_actions = [a.strip() for a in only_match.group(1).split(',')] elif except_match: excluded = [a.strip() for a in except_match.group(1).split(',')] allowed_actions = [a for a in allowed_actions if a not in excluded] # Build path prefix from namespace stack + resource stack path_prefix = '/'.join(namespace_stack + resource_stack) id_param = f':{resource_name}_id' if resource_name != resource_stack[-1] if resource_stack else f':id' # Map actions to routes action_routes = { 'index': ('GET', f'/api/{path_prefix}/{resource_name}'), 'show': ('GET', f'/api/{path_prefix}/{resource_name}/:{resource_name[:-1] if resource_name.endswith("s") else resource_name}_id' if resource_name.endswith('s') else f'/api/{path_prefix}/{resource_name}/:id'), 'create': ('POST', f'/api/{path_prefix}/{resource_name}'), 'update': ('PUT', f'/api/{path_prefix}/{resource_name}/:{resource_name[:-1] if resource_name.endswith("s") else resource_name}_id' if resource_name.endswith('s') else f'/api/{path_prefix}/{resource_name}/:id'), 'destroy': ('DELETE', f'/api/{path_prefix}/{resource_name}/:{resource_name[:-1] if resource_name.endswith("s") else resource_name}_id' if resource_name.endswith('s') else f'/api/{path_prefix}/{resource_name}/:id'), } # Simplify: just record the resource with its nesting routes.append(f'CRUD:{"/".join(namespace_stack + resource_stack)}/{resource_name}') i += 1 continue # Detect member block if 'member' in line and 'do' in line: in_member = True i += 1 continue # Detect collection block if 'collection' in line and 'do' in line: in_collection = True i += 1 continue # Detect explicit route definitions (get/post/put/patch/delete) route_match = re.match(r'(get|post|put|patch|delete)\s+[\'":]([^\'":\s]+)[\'":]', line) if route_match: method = route_match.group(1).upper() path = route_match.group(2) routes.append(f'{method}:{"/".join(namespace_stack)}/{path}') i += 1 continue # Detect end blocks (pop from stacks) if line == 'end': if in_member: in_member = False elif in_collection: in_collection = False elif resource_stack: resource_stack.pop() elif namespace_stack: namespace_stack.pop() i += 1 continue i += 1 # Print what we found print("=== Parsed Chatwoot routes.rb ===") for r in routes: print(r) print(f"\nTotal route definitions found: {len(routes)}")