Skip to content

Adding New Frameworks

GrayKwon edited this page Sep 18, 2025 · 2 revisions

Adding New Frameworks

Complete guide for implementing support for new web frameworks in endpoint.nvim using the modern OOP architecture.

Architecture Overview

endpoint.nvim uses a modern Object-Oriented architecture with clear separation of concerns:

  • Framework: Base class for all framework implementations
  • Parser: Handles endpoint parsing specific to each framework
  • Detector: Unified detection system (integrated into Framework)
  • EndpointManager: Manages all registered frameworks

Framework Structure

Each framework consists of two main components:

  1. Framework Class: Inherits from base Framework class
  2. Parser Class: Inherits from base Parser class

Implementation Steps

1. Create Parser Class

Create lua/endpoint/parser/yourframework_parser.lua:

local Parser = require "endpoint.core.Parser"

---@class endpoint.YourFrameworkParser
local YourFrameworkParser = setmetatable({}, { __index = Parser })
YourFrameworkParser.__index = YourFrameworkParser

---Creates a new YourFrameworkParser instance
function YourFrameworkParser:new()
  local parser = Parser:new("yourframework_parser", {
    framework_name = "yourframework",
    language = "javascript", -- or your language
  })
  setmetatable(parser, self)
  return parser
end

---Extract endpoint path from content (PRIVATE)
function YourFrameworkParser:_extract_endpoint_path(content)
  -- Pattern 1: @Route("/path")
  local path = content:match '@Route%(["\']([^"\']+)["\']'
  if path then
    return path
  end

  -- Pattern 2: yourframework.get("/path", handler)
  path = content:match 'yourframework%.%w+%(["\']([^"\']+)["\']'
  if path then
    return path
  end

  return nil
end

---Extract HTTP method from content (PRIVATE)
function YourFrameworkParser:_extract_method(content)
  -- Pattern 1: @Get annotation
  local method = content:match '@(%w+)%('
  if method and method:match("^(Get|Post|Put|Delete|Patch)$") then
    return method:upper()
  end

  -- Pattern 2: yourframework.method() call
  method = content:match 'yourframework%.(%w+)%('
  if method and method:match("^(get|post|put|delete|patch)$") then
    return method:upper()
  end

  return nil
end

---Create endpoint metadata (PRIVATE)
function YourFrameworkParser:_create_endpoint_metadata(file_path, parsed_data)
  return {
    framework = "yourframework",
    language = "javascript",
    controller_name = self:_extract_controller_name(file_path),
    -- Add framework-specific metadata
  }
end

return YourFrameworkParser

2. Create Framework Class

Create lua/endpoint/frameworks/yourframework.lua:

local Framework = require "endpoint.core.Framework"
local YourFrameworkParser = require "endpoint.parser.yourframework_parser"

---@class endpoint.YourFrameworkFramework
local YourFrameworkFramework = setmetatable({}, { __index = Framework })
YourFrameworkFramework.__index = YourFrameworkFramework

---Creates a new YourFrameworkFramework instance
function YourFrameworkFramework:new()
  local yourframework_instance = Framework.new(self, "yourframework", {
    file_extensions = { "*.js", "*.ts" },
    exclude_patterns = { "**/node_modules", "**/dist", "**/build" },
    patterns = {
      GET = { "@Get", "yourframework\\.get\\(" },
      POST = { "@Post", "yourframework\\.post\\(" },
      PUT = { "@Put", "yourframework\\.put\\(" },
      DELETE = { "@Delete", "yourframework\\.delete\\(" },
      PATCH = { "@Patch", "yourframework\\.patch\\(" },
    },
    search_options = { "--case-sensitive", "--type", "js" },
    controller_extractors = {
      {
        pattern = "([^/]+)%.%w+$",
        transform = function(name)
          return name:gsub("Controller$", ""):gsub("Routes$", ""):gsub("Router$", "")
        end
      }
    },
    detector = {
      dependencies = { "yourframework", "@yourframework/core" },
      manifest_files = { "package.json", "yourframework.config.js", "yourframework.json" },
      name = "yourframework_dependency_detection"
    },
    parser = YourFrameworkParser
  })
  setmetatable(yourframework_instance, self)
  return yourframework_instance
end

return YourFrameworkFramework

3. Register Framework

Add your framework to lua/endpoint/manager/EndpointManager.lua:

-- At the top with other imports
local YourFrameworkFramework = require "endpoint.frameworks.yourframework"

-- In the constructor, add your framework to the frameworks table
function EndpointManager:new()
  local manager = setmetatable({}, self)
  manager.frameworks = {
    -- existing frameworks...
    yourframework = YourFrameworkFramework:new(),
  }
  return manager
end

4. Add Type Definitions

Add to meta/types.lua:

---@class endpoint.YourFrameworkFramework : endpoint.Framework
---@field new fun(self: endpoint.YourFrameworkFramework): endpoint.YourFrameworkFramework

---@class endpoint.YourFrameworkParser : endpoint.Parser
---@field new fun(self: endpoint.YourFrameworkParser): endpoint.YourFrameworkParser

5. Create Test Fixtures

Create a realistic test project in tests/fixtures/yourframework/:

tests/fixtures/yourframework/
├── package.json
├── yourframework.config.js
├── src/
│   ├── controllers/
│   │   ├── UserController.js
│   │   ├── ProductController.js
│   │   └── OrderController.js
│   └── routes/
│       └── api.js
└── README.md

Example controller file (UserController.js):

// @Route("/api/users")
class UserController {
    // @Get("/")
    getAllUsers() { }

    // @Get("/{id}")
    getUser(id) { }

    // @Post("/")
    createUser(data) { }

    // @Put("/{id}")
    updateUser(id, data) { }

    // @Delete("/{id}")
    deleteUser(id) { }
}

Example route file (api.js):

const express = require('express');
const router = express.Router();

yourframework.get('/health', (req, res) => {
    res.json({ status: 'ok' });
});

yourframework.post('/webhook', (req, res) => {
    // Handle webhook
});

6. Write Comprehensive Tests

Create tests/spec/yourframework_spec.lua:

local YourFrameworkFramework = require "endpoint.frameworks.yourframework"
local YourFrameworkParser = require "endpoint.parser.yourframework_parser"

describe("YourFramework Framework", function()
  local framework
  local parser

  before_each(function()
    framework = YourFrameworkFramework:new()
    parser = YourFrameworkParser:new()
  end)

  describe("framework detection", function()
    it("should detect yourframework projects", function()
      -- Test detection logic
      local detected = framework:detect()
      assert.is_boolean(detected)
    end)
  end)

  describe("parser functionality", function()
    it("should parse @Get annotation", function()
      local content = '@Get("/users")'
      local method = parser:_extract_method(content)
      local path = parser:_extract_endpoint_path(content)

      assert.equals("GET", method)
      assert.equals("/users", path)
    end)

    it("should parse framework method calls", function()
      local content = 'yourframework.get("/health", handler)'
      local method = parser:_extract_method(content)
      local path = parser:_extract_endpoint_path(content)

      assert.equals("GET", method)
      assert.equals("/health", path)
    end)
  end)

  describe("integration tests", function()
    it("should find endpoints in test fixtures", function()
      -- Test with actual fixture files
      local endpoints = framework:scan()
      assert.is_not_nil(endpoints)
      assert.is_table(endpoints)
    end)
  end)
end)

Configuration Options

File Processing

Configure which files to include/exclude:

file_extensions = { "*.js", "*.ts", "*.jsx", "*.tsx" },
exclude_patterns = {
  "**/node_modules",
  "**/dist",
  "**/build",
  "**/.next"
},

Search Patterns

Define patterns for finding endpoints:

patterns = {
  GET = { "@Get", "@GetMapping", "yourframework\\.get\\(" },
  POST = { "@Post", "@PostMapping", "yourframework\\.post\\(" },
  -- Add more as needed
},

Search Options

Optimize ripgrep performance:

search_options = {
  "--case-sensitive",  -- Preserve annotation casing
  "--type", "js",      -- Limit to specific file types
  "--max-depth", "10"  -- Limit search depth
},

Controller Name Extraction

Extract meaningful controller names from file paths:

controller_extractors = {
  {
    pattern = "([^/]+)Controller%.js$",  -- Match "UserController.js"
    transform = function(name)
      return name:gsub("Controller$", "") -- Return "User"
    end
  },
  {
    pattern = "routes/([^/]+)%.js$",     -- Match "routes/users.js"
    transform = function(name)
      return name:gsub("Routes$", ""):gsub("Router$", "")
    end
  }
},

Advanced Patterns

Complex Route Parsing

For frameworks with complex routing patterns:

function YourFrameworkParser:_extract_endpoint_path(content)
  -- Pattern 1: Simple annotation
  local path = content:match '@Route%(["\']([^"\']+)["\']'
  if path then return path end

  -- Pattern 2: With parameters
  path = content:match '@Route%(["\']([^"\']+)["\'].*method'
  if path then return path end

  -- Pattern 3: Functional style
  path = content:match 'route%(["\']([^"\']+)["\']'
  if path then return path end

  -- Pattern 4: Object-based configuration
  local route_obj = content:match 'route:%s*{([^}]+)}'
  if route_obj then
    path = route_obj:match 'path:%s*["\']([^"\']+)["\']'
    if path then return path end
  end

  return nil
end

Framework Detection

Robust detection with multiple strategies:

detector = {
  dependencies = {
    "yourframework",
    "@yourframework/core",
    "yourframework-cli"
  },
  manifest_files = {
    "package.json",           -- Primary detection
    "yourframework.config.js", -- Framework config
    "yourframework.json",     -- Alternative config
    "src/",                   -- Source directory
    ".yourframeworkrc"        -- RC file
  },
  name = "yourframework_dependency_detection"
},

Testing Your Implementation

Manual Testing

# 1. Test framework detection
cd tests/fixtures/yourframework
nvim -c "lua print('Detected:', require('endpoint.frameworks.yourframework'):new():detect())" -c "qa"

# 2. Test parser
nvim -c "lua
local parser = require('endpoint.parser.yourframework_parser'):new()
local result = parser:parse_content('@Get(\"/test\")', 'test.js', 1, 1)
print('Parsed:', vim.inspect(result))
" -c "qa"

# 3. Run framework tests
make test-yourframework

Integration Testing

# Test with actual projects
cd /path/to/real/yourframework/project
nvim -c "Endpoint"

Common Patterns by Language

JavaScript/TypeScript Frameworks

  • Package.json dependency detection
  • Decorator/annotation parsing
  • Method chaining patterns

Java/Kotlin Frameworks

  • Maven/Gradle dependency detection
  • Annotation-based routing
  • Spring-style path mapping

Python Frameworks

  • requirements.txt/pyproject.toml detection
  • Decorator-based routing
  • Blueprint/router patterns

Ruby Frameworks

  • Gemfile dependency detection
  • DSL-style routing
  • Convention-based patterns

Troubleshooting

Framework Not Detected

  1. Check detector configuration
  2. Verify dependency names in manifest files
  3. Test detection logic in isolation

No Endpoints Found

  1. Verify search patterns match your framework's syntax
  2. Test ripgrep command manually
  3. Check file extension filters

Parsing Issues

  1. Test parser methods with sample content
  2. Verify regex patterns capture correct groups
  3. Handle edge cases (no path, different quotes)

Performance Issues

  1. Optimize search patterns for specificity
  2. Use appropriate file type filters
  3. Exclude unnecessary directories

Best Practices

Code Organization

  • Keep parser logic separate from framework logic
  • Use private methods (prefix with _) for internal operations
  • Follow consistent naming conventions

Pattern Matching

  • Start with most specific patterns
  • Handle multiple syntax variations
  • Use non-greedy matching where appropriate

Error Handling

  • Gracefully handle missing or malformed content
  • Return nil for unparseable content
  • Log debug information when appropriate

Testing

  • Test both positive and negative cases
  • Include edge cases in test fixtures
  • Verify integration with EndpointManager

Example Framework Implementations

Study existing implementations for guidance:

  • Simple: FastAPI (Python decorators)
  • Moderate: Express (method chaining)
  • Complex: Spring Boot (annotation combinations)
  • Advanced: NestJS (TypeScript decorators with metadata)