-
Notifications
You must be signed in to change notification settings - Fork 2
Adding New Frameworks
Complete guide for implementing support for new web frameworks in endpoint.nvim using the modern OOP architecture.
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
Each framework consists of two main components:
-
Framework Class: Inherits from base
Frameworkclass -
Parser Class: Inherits from base
Parserclass
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 YourFrameworkParserCreate 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 YourFrameworkFrameworkAdd 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
endAdd 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.YourFrameworkParserCreate 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
});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)Configure which files to include/exclude:
file_extensions = { "*.js", "*.ts", "*.jsx", "*.tsx" },
exclude_patterns = {
"**/node_modules",
"**/dist",
"**/build",
"**/.next"
},Define patterns for finding endpoints:
patterns = {
GET = { "@Get", "@GetMapping", "yourframework\\.get\\(" },
POST = { "@Post", "@PostMapping", "yourframework\\.post\\(" },
-- Add more as needed
},Optimize ripgrep performance:
search_options = {
"--case-sensitive", -- Preserve annotation casing
"--type", "js", -- Limit to specific file types
"--max-depth", "10" -- Limit search depth
},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
}
},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
endRobust 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"
},# 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# Test with actual projects
cd /path/to/real/yourframework/project
nvim -c "Endpoint"- Package.json dependency detection
- Decorator/annotation parsing
- Method chaining patterns
- Maven/Gradle dependency detection
- Annotation-based routing
- Spring-style path mapping
- requirements.txt/pyproject.toml detection
- Decorator-based routing
- Blueprint/router patterns
- Gemfile dependency detection
- DSL-style routing
- Convention-based patterns
- Check detector configuration
- Verify dependency names in manifest files
- Test detection logic in isolation
- Verify search patterns match your framework's syntax
- Test ripgrep command manually
- Check file extension filters
- Test parser methods with sample content
- Verify regex patterns capture correct groups
- Handle edge cases (no path, different quotes)
- Optimize search patterns for specificity
- Use appropriate file type filters
- Exclude unnecessary directories
- Keep parser logic separate from framework logic
- Use private methods (prefix with
_) for internal operations - Follow consistent naming conventions
- Start with most specific patterns
- Handle multiple syntax variations
- Use non-greedy matching where appropriate
- Gracefully handle missing or malformed content
- Return
nilfor unparseable content - Log debug information when appropriate
- Test both positive and negative cases
- Include edge cases in test fixtures
- Verify integration with EndpointManager
Study existing implementations for guidance:
- Simple: FastAPI (Python decorators)
- Moderate: Express (method chaining)
- Complex: Spring Boot (annotation combinations)
- Advanced: NestJS (TypeScript decorators with metadata)