Adding New Features
This guide explains how to add new features to Forge, from configuration collection to code generation.
Overview
Adding a feature involves these steps:
Add configuration collection in
commands/init.pyExtend
ConfigReaderwith helper methodsCreate generators for the feature
Update dependencies in the pyproject generator
Add tests
Update documentation
Example: Adding GraphQL Support
Let’s walk through adding GraphQL support as an example.
Step 1: Configuration Collection
Edit commands/init.py to add the interactive prompt:
def collect_features() -> dict:
# ... existing prompts ...
# Add GraphQL prompt
has_graphql = questionary.confirm(
"Enable GraphQL support?",
default=False,
style=custom_style
).ask()
return {
# ... existing features ...
"graphql": has_graphql,
}
The configuration will be saved to .forge/config.json:
{
"features": {
"graphql": true
}
}
Step 2: Extend ConfigReader
Edit core/config_reader.py to add a helper method:
class ConfigReader:
# ... existing methods ...
def has_graphql(self) -> bool:
"""Check if GraphQL is enabled"""
return self.config.get("features", {}).get("graphql", False)
Step 3: Create Generators
Create a new directory for GraphQL generators:
core/generators/templates/graphql/
├── __init__.py
├── schema.py
└── router.py
GraphQL Schema Generator
# core/generators/templates/graphql/schema.py
from core.decorators import Generator
from core.generators.templates.base import BaseTemplateGenerator
@Generator(
category="graphql",
priority=75,
requires=["UserModelGenerator"],
enabled_when=lambda c: c.has_graphql(),
description="Generate GraphQL schema (app/graphql/schema.py)"
)
class GraphQLSchemaGenerator(BaseTemplateGenerator):
"""GraphQL schema generator"""
def generate(self) -> None:
imports = [
"import strawberry",
"from typing import List, Optional",
"from app.models.user import User as UserModel",
]
content = '''@strawberry.type
class User:
"""GraphQL User type"""
id: int
username: str
email: str
is_active: bool
@classmethod
def from_model(cls, model: UserModel) -> "User":
return cls(
id=model.id,
username=model.username,
email=model.email,
is_active=model.is_active,
)
@strawberry.type
class Query:
@strawberry.field
async def users(self, info) -> List[User]:
"""Get all users"""
db = info.context["db"]
from app.crud.user import user_crud
users = await user_crud.get_all(db)
return [User.from_model(u) for u in users]
@strawberry.field
async def user(self, info, user_id: int) -> Optional[User]:
"""Get user by ID"""
db = info.context["db"]
from app.crud.user import user_crud
user = await user_crud.get_by_id(db, user_id)
return User.from_model(user) if user else None
schema = strawberry.Schema(query=Query)
'''
# Create directory
(self.project_path / "app" / "graphql").mkdir(parents=True, exist_ok=True)
# Create __init__.py
self.file_ops.create_file(
"app/graphql/__init__.py",
content="",
overwrite=False
)
# Create schema file
self.file_ops.create_python_file(
file_path="app/graphql/schema.py",
docstring="GraphQL schema definition",
imports=imports,
content=content,
overwrite=True
)
GraphQL Router Generator
# core/generators/templates/graphql/router.py
from core.decorators import Generator
from core.generators.templates.base import BaseTemplateGenerator
@Generator(
category="graphql",
priority=85,
requires=["GraphQLSchemaGenerator"],
enabled_when=lambda c: c.has_graphql(),
description="Generate GraphQL router (app/routers/graphql.py)"
)
class GraphQLRouterGenerator(BaseTemplateGenerator):
"""GraphQL router generator"""
def generate(self) -> None:
imports = [
"from fastapi import Depends",
"from strawberry.fastapi import GraphQLRouter",
"from sqlalchemy.ext.asyncio import AsyncSession",
"",
"from app.core.database import get_db",
"from app.graphql.schema import schema",
]
content = '''async def get_context(db: AsyncSession = Depends(get_db)):
"""Provide database session to GraphQL resolvers"""
return {"db": db}
graphql_router = GraphQLRouter(
schema,
context_getter=get_context,
)
'''
self.file_ops.create_python_file(
file_path="app/routers/graphql.py",
docstring="GraphQL router configuration",
imports=imports,
content=content,
overwrite=True
)
Step 4: Update Dependencies
Edit core/generators/configs/pyproject.py to add GraphQL dependencies:
def _get_dependencies(self) -> list:
dependencies = [
# ... existing dependencies ...
]
# GraphQL dependencies
if self.config_reader.has_graphql():
dependencies.extend([
'strawberry-graphql[fastapi]>=0.200.0',
])
return dependencies
Step 5: Update Main Generator
The main.py generator needs to include the GraphQL router. Edit core/generators/templates/app/main.py:
def _generate_main_with_graphql(self) -> None:
# Add import
if self.config_reader.has_graphql():
imports.append("from app.routers.graphql import graphql_router")
# Add router
if self.config_reader.has_graphql():
app_content += '''
# GraphQL endpoint
app.include_router(graphql_router, prefix="/graphql")
'''
Step 6: Add Tests
Create tests for the new feature:
# tests/test_graphql_generator.py
import pytest
from pathlib import Path
from core.config_reader import ConfigReader
from core.generators.templates.graphql.schema import GraphQLSchemaGenerator
def test_graphql_generator_creates_files(tmp_path):
"""Test GraphQL generator creates schema file"""
# Setup config
config_dir = tmp_path / ".forge"
config_dir.mkdir()
(config_dir / "config.json").write_text(
'{"features": {"graphql": true, "auth": {"type": "basic"}}}'
)
# Create required directories
(tmp_path / "app" / "graphql").mkdir(parents=True)
# Run generator
config_reader = ConfigReader(tmp_path)
generator = GraphQLSchemaGenerator(tmp_path, config_reader)
generator.generate()
# Verify
assert (tmp_path / "app" / "graphql" / "schema.py").exists()
Step 7: Update Documentation
Add documentation for the new feature:
Create
docs/user-guide/graphql.mdAdd to
docs/index.rsttoctreeUpdate feature list in relevant docs
Checklist for New Features
Configuration prompt in
commands/init.pyHelper method in
ConfigReaderGenerator(s) with proper decorator parameters
Dependencies in pyproject generator
Integration with main.py (if needed)
Tests for generators
Documentation
Tips
Start simple: Create a minimal working generator first
Follow patterns: Look at existing generators for examples
Test incrementally: Run
forge initfrequently to verify outputCheck dependencies: Ensure
requireslists all prerequisitesUse conditions: Use
enabled_whento skip generators when feature is disabled