#!/usr/bin/env python3 """Parse Chatwoot routes.rb and generate full route list comparable to GoChat dump_routes output.""" import re import sys def main(): with open('/home/yanghao05/Workspace/chatwoot-reference/config/routes.rb', 'r') as f: content = f.read() lines = content.split('\n') namespace_stack = [] # (name, indent_level) routes = [] def current_prefix(): parts = [ns for ns, _ in namespace_stack] if parts: return '/' + '/'.join(parts) return '' # Method mapping for Rails actions method_map = { 'index': 'GET', 'show': 'GET', 'create': 'POST', 'update': 'PATCH', 'destroy': 'DELETE', 'new': 'GET', 'edit': 'GET' } # Parse each line i = 0 while i < len(lines): line = lines[i] stripped = line.strip() indent = len(line) - len(line.lstrip()) # Namespace ns_match = re.match(r'namespace\s+:(\w+)', stripped) if ns_match: ns = ns_match.group(1) namespace_stack.append((ns, indent)) i += 1 continue # scope module: scope_match = re.match(r'scope\s+module:\s+:(\w+)', stripped) if scope_match: mod = scope_match.group(1) namespace_stack.append(('__scope_' + mod, indent)) i += 1 continue # End block if stripped == 'end': if namespace_stack and namespace_stack[-1][1] >= indent: namespace_stack.pop() i += 1 continue # resources / resource res_match = re.match(r'(resources|resource)\s+:([a-zA-Z_]\w*)', stripped) if res_match: kind = res_match.group(1) name = res_match.group(2) is_singular = (kind == 'resource') # Extract only constraint only_match = re.search(r'only:\s*\[([^\]]+)\]', stripped) if only_match: actions = [a.strip().strip(':').strip("'").strip('"') for a in only_match.group(1).split(',')] else: if is_singular: actions = ['show', 'create', 'update', 'destroy'] else: actions = ['index', 'show', 'create', 'update', 'destroy', 'new', 'edit'] # Extract except constraint except_match = re.search(r'except:\s*\[([^\]]+)\]', stripped) if except_match: excluded = [a.strip().strip(':').strip("'").strip('"') for a in except_match.group(1).split(',')] actions = [a for a in actions if a not in excluded] prefix = current_prefix() for action in actions: method = method_map.get(action, 'GET') if is_singular: path = prefix + '/' + name elif action in ['index', 'create', 'new']: path = prefix + '/' + name else: path = prefix + '/' + name + '/:id' routes.append((method, path, action)) i += 1 continue # member do block - look for routes inside # collection do block # Direct route definitions direct_match = re.match(r"(get|post|put|patch|delete)\s+'([^']+)'", stripped) if direct_match: method = direct_match.group(1).upper() path = direct_match.group(2) routes.append((method, current_prefix() + path, '')) i += 1 continue # member action: post :action, on: :member member_action = re.match(r"(get|post|put|patch|delete)\s+:(\w+)", stripped) if member_action and not res_match: method = member_action.group(1).upper() action = member_action.group(2) routes.append((method, current_prefix() + '/:action_' + action, action)) i += 1 continue i += 1 # Deduplicate unique_routes = sorted(set(routes)) print(f"Total unique routes: {len(unique_routes)}") for method, path, action in unique_routes: print(f"{method} {path}") if __name__ == '__main__': main()