✨ feat(vscode): 代码阅读的 FastAPI 路由器单独一张「路由器」卡片(前缀、标签、依赖、默认响应,接口表列挂在它上面的接口,函数名 ⌘ + 点击跳到接口卡片);接口卡片写所属路由器
This commit is contained in:
1 parent
b4e4879570
commit
71641bdc41
6 files changed
+282
-30
No files matched your search
@@ -444,6 +444,7 @@ export const KIND_ICON_NAMES = [
|
||||
'server-cog',
|
||||
'calendar-clock',
|
||||
'globe',
|
||||
'network',
|
||||
] as const
|
||||
|
||||
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
expect(propertyLines(properties(card))).toEqual([
|
||||
'名称: update_user',
|
||||
'说明: 更新用户信息。\n\n管理员可更新任意用户,普通用户只能更新自己的基础资料。',
|
||||
'路由器: router',
|
||||
'标签: 用户管理',
|
||||
'异步: 是',
|
||||
'依赖: get_db(db)、get_current_user(current_user)',
|
||||
@@ -82,7 +83,7 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
' 403 | Forbidden | 无权更新此用户信息;无权修改角色;无权修改启用状态;无权修改密码',
|
||||
' 422 | 参数校验失败 | FastAPI 自动返回',
|
||||
])
|
||||
expect(properties(card).map(row => row.columns)).toEqual([undefined, undefined, undefined, undefined, undefined, ['项', '值'], ['状态码', '内容', '说明']])
|
||||
expect(properties(card).map(row => row.columns)).toEqual([undefined, undefined, undefined, undefined, undefined, undefined, ['项', '值'], ['状态码', '内容', '说明']])
|
||||
expect(card.attributes).toMatchObject({ kind: 'fastapi.route', access: 'public', async: 'true', httpMethod: 'PUT' })
|
||||
expect(fastapiRule.attributeNames?.('zh')).toEqual({ attributes: { httpMethod: '请求方法' }, values: {} })
|
||||
expect(ROUTES.slice(card.defines?.startIndex, card.defines?.endIndex)).toBe('update_user')
|
||||
@@ -123,6 +124,7 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
it('luffy 的 list_skills:Annotated 里的 Query 是查询参数(类型取 Annotated 的第一项),Depends 是依赖,参数名是 _ 的只写依赖', async () => {
|
||||
expect(propertyLines(properties(await route(SKILL_HUB, 'list_skills')))).toEqual([
|
||||
'名称: list_skills',
|
||||
'路由器: skill_hub_router',
|
||||
'标签: skill-hub',
|
||||
'异步: 是',
|
||||
'依赖: get_db(db)、get_current_user',
|
||||
@@ -142,7 +144,7 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
' 404 | HTTPException | f"技能不存在: {slug}"',
|
||||
' 422 | 参数校验失败 | FastAPI 自动返回',
|
||||
])
|
||||
expect(propertyLines(properties(await route(SKILL_HUB, 'install'))).slice(4)).toEqual([
|
||||
expect(propertyLines(properties(await route(SKILL_HUB, 'install'))).slice(5)).toEqual([
|
||||
'请求: POST /skill-hub/apps/{app_id}/skills/{slug}',
|
||||
' URL | /skill-hub/apps/{app_id}/skills/{slug}',
|
||||
' 方法 | POST',
|
||||
@@ -157,6 +159,7 @@ describe('fastapi 库规则:接口卡片', () => {
|
||||
expect(propertyLines(properties(await route(CONSOLE, 'create_user', known({ Forbidden: '403' }))))).toEqual([
|
||||
'名称: create_user',
|
||||
'说明: 创建新用户。\n\n需要管理员权限。密码会自动哈希。',
|
||||
'路由器: router',
|
||||
'标签: 用户管理-运维',
|
||||
'异步: 是',
|
||||
'依赖: get_db(db)、require_users_create',
|
||||
@@ -239,6 +242,7 @@ def remove(item_id: int):
|
||||
expect(propertyLines(properties(await route(source, 'remove')))).toEqual([
|
||||
'名称: remove',
|
||||
'说明: 删掉一个物品。',
|
||||
'路由器: router',
|
||||
'标签: 物品、管理',
|
||||
'异步: 否',
|
||||
'依赖: verify_token、audit',
|
||||
@@ -265,6 +269,7 @@ async def health() -> dict:
|
||||
`
|
||||
expect(propertyLines(properties(await route(source, 'health')))).toEqual([
|
||||
'名称: health',
|
||||
'路由器: app',
|
||||
'异步: 是',
|
||||
'装饰器: @cache(60)',
|
||||
'请求: GET /health',
|
||||
@@ -303,6 +308,84 @@ def e():
|
||||
})
|
||||
})
|
||||
|
||||
/** 名字是 name 的路由器卡片。 */
|
||||
async function routerCard(source: string, name: string): Promise<BlockView> {
|
||||
const found = flatten(await views(source)).find(view => view.kind === 'fastapi.router' && view.defines && source.slice(view.defines.startIndex, view.defines.endIndex) === name)
|
||||
if (!found)
|
||||
throw new Error(`没有 ${name} 的路由器卡片`)
|
||||
return found
|
||||
}
|
||||
|
||||
describe('fastapi 库规则:路由器卡片', () => {
|
||||
it('luffy 的 router:名称 / 前缀 / 标签,接口表列这份文件里挂在它上面的接口(方法 / 完整路径 / 函数 / docstring 第一行)', async () => {
|
||||
const card = await routerCard(ROUTES, 'router')
|
||||
expect(card.badge).toEqual({ label: '路由器', color: { dark: '#7aa2f7', light: '#34548a' }, icon: 'network' })
|
||||
expect(propertyLines(properties(card))).toEqual([
|
||||
'名称: router',
|
||||
'前缀: /users',
|
||||
'标签: 用户管理',
|
||||
'接口: 3 个',
|
||||
' GET | /users/{user_id} | get_user | 获取用户详情。',
|
||||
' PUT | /users/{user_id} | update_user | 更新用户信息。',
|
||||
' POST | /users/me/avatar | upload_current_user_avatar | 上传当前登录用户头像。',
|
||||
])
|
||||
expect(properties(card).at(-1)?.columns).toEqual(['方法', '路径', '函数', '说明'])
|
||||
expect(card.code).toBe('router = APIRouter(tags=["用户管理"], prefix="/users")')
|
||||
expect(ROUTES.slice(card.defines?.startIndex, card.defines?.endIndex)).toBe('router')
|
||||
})
|
||||
|
||||
it('函数名带下标(⌘ + 点击跳到接口卡片);路径里的 {user_id} 各讲各的函数的参数;方法用 Swagger UI 的颜色', async () => {
|
||||
const [getUser, updateUser] = properties(await routerCard(ROUTES, 'router')).at(-1)?.items ?? []
|
||||
expect(getUser.cells[2]).toEqual([{ text: 'get_user', role: 'function', offset: ROUTES.indexOf('get_user(') }])
|
||||
const paramOf = (fn: string): number => ROUTES.indexOf('user_id: Int4Path', ROUTES.indexOf(`def ${fn}`))
|
||||
for (const [row, fn] of [[getUser, 'get_user'], [updateUser, 'update_user']] as const)
|
||||
expect(flatSegments(row.cells[1]).find(piece => piece.text === 'user_id')?.refers).toEqual({ startIndex: paramOf(fn), endIndex: paramOf(fn) + 'user_id'.length })
|
||||
expect(updateUser.cells[0]).toEqual([{ text: 'PUT', role: 'keyword', color: { dark: '#fca130', light: '#b35f00' } }])
|
||||
})
|
||||
|
||||
it('接口卡片写挂在哪个路由器上(名字带下标,⌘ + 点击跳到路由器卡片),排在说明后面', async () => {
|
||||
const rows = properties(await route(ROUTES, 'update_user'))
|
||||
expect(rows.map(row => row.label).slice(0, 4)).toEqual(['名称', '说明', '路由器', '标签'])
|
||||
expect(rows[2].value).toEqual([{ text: 'router', role: 'variable', offset: ROUTES.indexOf('router = APIRouter') }])
|
||||
})
|
||||
|
||||
it('写了依赖、默认响应才有这两行;FastAPI 应用没有前缀;变量的说明是属性 docstring;没挂接口时「0 个」、不画表格', async () => {
|
||||
const source = `${HEADER}"""整个应用。"""
|
||||
empty = APIRouter()
|
||||
|
||||
@router.get("/{item_id}")
|
||||
def show(item_id: int):
|
||||
"""看一个物品。
|
||||
|
||||
带库存。
|
||||
"""
|
||||
|
||||
@app.post("/items")
|
||||
def create():
|
||||
pass
|
||||
`
|
||||
expect(propertyLines(properties(await routerCard(source, 'router')))).toEqual([
|
||||
'名称: router',
|
||||
'前缀: /items',
|
||||
'标签: 物品',
|
||||
'依赖: verify_token',
|
||||
'默认响应: 401',
|
||||
' 401 | | 没登录',
|
||||
'接口: 1 个',
|
||||
' GET | /items/{item_id} | show | 看一个物品。',
|
||||
])
|
||||
expect(propertyLines(properties(await routerCard(source, 'app')))).toEqual([
|
||||
'名称: app',
|
||||
'说明: 整个应用。',
|
||||
'接口: 1 个',
|
||||
' POST | /items | create | ',
|
||||
])
|
||||
const empty = properties(await routerCard(source, 'empty'))
|
||||
expect(propertyLines(empty)).toEqual(['名称: empty', '接口: 0 个'])
|
||||
expect(empty.at(-1)?.columns).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fastapi 库规则:别的文件里的定义(工作区索引 + 语言扩展查定义)', () => {
|
||||
const SCHEMAS = 'backend/src/luffy_agent/core/exceptions/schemas.py'
|
||||
const PARAMS = 'backend/src/luffy_agent/core/params.py'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, VariableBlock, WorkspaceIndex } from '../contract'
|
||||
import type { Call, LibraryNames } from './pythonTree'
|
||||
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
|
||||
import { hint, joined, name, range, word } from './segments'
|
||||
@@ -15,6 +15,9 @@ const RULE_ID = 'fastapi'
|
||||
const KIND = 'fastapi.route'
|
||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||
const ICON: KindIconName = 'globe'
|
||||
const ROUTER_KIND = 'fastapi.router'
|
||||
const ROUTER_COLOR: ThemeColor = { dark: '#7aa2f7', light: '#34548a' }
|
||||
const ROUTER_ICON: KindIconName = 'network'
|
||||
/** 认路由对象、参数标记、HTTPException 时看从这几个库导入的名字(HTTPException、status 也可以从 starlette 来)。 */
|
||||
const NAME_LIBRARIES = ['fastapi', 'starlette']
|
||||
|
||||
@@ -63,12 +66,17 @@ type Place = 'path' | 'query' | 'header' | 'cookie' | 'body' | 'dependency'
|
||||
|
||||
/** 一种显示语言的用词。 */
|
||||
interface Words {
|
||||
/** 接口卡片、路由器卡片的种类名 */
|
||||
kind: string
|
||||
labels: Record<'name' | 'doc' | 'tags' | 'async' | 'decorators' | 'dependencies' | 'request' | 'responses', string>
|
||||
routerKind: string
|
||||
labels: Record<'name' | 'doc' | 'router' | 'tags' | 'async' | 'decorators' | 'dependencies' | 'request' | 'responses' | 'prefix' | 'defaultResponses' | 'routes', string>
|
||||
yes: string
|
||||
no: string
|
||||
count: (n: number) => string
|
||||
requestHeads: string[]
|
||||
responseHeads: string[]
|
||||
/** 路由器卡片的接口表:方法 | 路径 | 函数 | 说明 */
|
||||
routeHeads: string[]
|
||||
items: Record<'url' | 'method' | 'path' | 'query' | 'header' | 'cookie' | 'body', string>
|
||||
/** 成功那一行没写 response_description 时的说明 */
|
||||
success: string
|
||||
@@ -86,11 +94,27 @@ interface Words {
|
||||
|
||||
const ZH: Words = {
|
||||
kind: '接口',
|
||||
labels: { name: '名称', doc: '说明', tags: '标签', async: '异步', decorators: '装饰器', dependencies: '依赖', request: '请求', responses: '响应' },
|
||||
routerKind: '路由器',
|
||||
labels: {
|
||||
name: '名称',
|
||||
doc: '说明',
|
||||
router: '路由器',
|
||||
tags: '标签',
|
||||
async: '异步',
|
||||
decorators: '装饰器',
|
||||
dependencies: '依赖',
|
||||
request: '请求',
|
||||
responses: '响应',
|
||||
prefix: '前缀',
|
||||
defaultResponses: '默认响应',
|
||||
routes: '接口',
|
||||
},
|
||||
yes: '是',
|
||||
no: '否',
|
||||
count: n => `${n} 个`,
|
||||
requestHeads: ['项', '值'],
|
||||
responseHeads: ['状态码', '内容', '说明'],
|
||||
routeHeads: ['方法', '路径', '函数', '说明'],
|
||||
items: { url: 'URL', method: '方法', path: '路径参数', query: '查询参数', header: '请求头', cookie: 'Cookie', body: '请求体' },
|
||||
success: '成功',
|
||||
validation: '参数校验失败',
|
||||
@@ -225,6 +249,8 @@ function listItems(node: Node | undefined): Node[] {
|
||||
|
||||
/** 路由对象:同一文件模块顶层 `X = APIRouter(...)` / `X = FastAPI(...)` 赋值出来的,和它给挂在上面的接口叠的东西。 */
|
||||
interface Router {
|
||||
/** 赋值目标:路由对象的名字 */
|
||||
target: Node
|
||||
prefix?: Node
|
||||
tags: Node[]
|
||||
dependencies: Node[]
|
||||
@@ -242,6 +268,7 @@ function routersOf(root: Node, env: Env): Map<string, Router> {
|
||||
if (target?.type !== 'identifier' || !call || (kind !== 'APIRouter' && kind !== 'FastAPI'))
|
||||
continue
|
||||
routers.set(target.text, {
|
||||
target,
|
||||
prefix: kind === 'APIRouter' ? call.keywords.get('prefix') : undefined,
|
||||
tags: listItems(call.keywords.get('tags')),
|
||||
dependencies: listItems(call.keywords.get('dependencies')),
|
||||
@@ -272,6 +299,40 @@ function routeOf(decorator: Node, routers: ReadonlyMap<string, Router>): Route |
|
||||
return { decorator, router, method: method.toUpperCase(), call, path: call.positional[0] ?? call.keywords.get('path') }
|
||||
}
|
||||
|
||||
/** 路由器卡片接口表里的一个接口:挂在这个路由对象上的函数(模块顶层的)。 */
|
||||
interface RouteOn {
|
||||
route: Route
|
||||
definition: Node
|
||||
name: Node
|
||||
}
|
||||
|
||||
/** 这份文件模块顶层挂在 router 上的接口,照源码顺序;一个函数几个路由装饰器时同接口卡片,取第一个。 */
|
||||
function routesOn(root: Node, routers: ReadonlyMap<string, Router>, router: Router): RouteOn[] {
|
||||
return syntaxChildren(root).flatMap((statement) => {
|
||||
const definition = statement.type === 'decorated_definition' ? statement.childForFieldName('definition') : null
|
||||
const name = definition?.type === 'function_definition' ? definition.childForFieldName('name') : null
|
||||
if (!definition || !name)
|
||||
return []
|
||||
const route = syntaxChildren(statement).filter(child => child.type === 'decorator').map(decorator => routeOf(decorator, routers)).find(found => found !== undefined)
|
||||
return route?.router === router ? [{ route, definition, name }] : []
|
||||
})
|
||||
}
|
||||
|
||||
/** 函数的参数名 → 定义处那个名字(`*args` 这种不算),路径里 `{参数}` 讲的是它。 */
|
||||
function parametersOf(definition: Node): Map<string, Node> {
|
||||
const names = syntaxChildren(definition.childForFieldName('parameters')).map(param =>
|
||||
param.type === 'identifier' ? param : param.type === 'typed_parameter' ? syntaxChildren(param)[0] : param.childForFieldName('name'))
|
||||
return new Map(names.flatMap(param => param?.type === 'identifier' ? [[param.text, param] as const] : []))
|
||||
}
|
||||
|
||||
/** 函数 docstring 的第一行(翻译插件取的说明原文),没有 docstring 时没有。 */
|
||||
function docFirstLine(definition: Node, env: Env): string | undefined {
|
||||
const [first] = syntaxChildren(definition.childForFieldName('body'))
|
||||
const [doc] = first?.type === 'expression_statement' ? syntaxChildren(first) : []
|
||||
const markdown = doc?.type === 'string' ? env.context.doc(doc)?.markdown : undefined
|
||||
return markdown?.split('\n').map(line => line.trim()).find(line => line !== '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径里的一段:`{参数}` 按变量着色,其余按字符串,都带源码下标;花括号里的名字讲的是函数的同名参数(params),
|
||||
* 标上 refers,高亮参数的各处出现时它也跟着亮、⌘ + 点击跳到参数。写的不是字符串时交给翻译插件翻。
|
||||
@@ -544,6 +605,25 @@ function addRaises(body: Node | null | undefined, statuses: Statuses, env: Env):
|
||||
}
|
||||
}
|
||||
|
||||
/** 响应那一行:值是各状态码用 ` / ` 连起来,下面一张表格「状态码 | 内容 | 说明」(同一行里的内容「、」隔开,消息「;」隔开)。 */
|
||||
function responsesRow(label: string, statuses: Statuses, env: Env): PropertyRow {
|
||||
const { words } = env
|
||||
const rows = statuses.rows()
|
||||
return {
|
||||
label,
|
||||
value: [hint(rows.map(row => textOf(row.code)).join(' / '))],
|
||||
columns: words.responseHeads,
|
||||
items: rows.map(row => ({
|
||||
cells: [row.code, joined(row.contents, hint(words.listSeparator)), joined(row.notes, { text: words.messageSeparator, role: 'comment' })],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/** HTTP 方法:大写,按 Swagger UI 的配色(没有配色的按关键字着色)。 */
|
||||
function methodText(method: string): Inline[] {
|
||||
return [{ text: method, role: 'keyword', ...(METHOD_COLORS[method] && { color: METHOD_COLORS[method] }) }]
|
||||
}
|
||||
|
||||
/** 请求表的一行:项 | 值。 */
|
||||
function requestItem(label: string, value: Inline[]): PropertyItem {
|
||||
return { cells: [[hint(label)], value] }
|
||||
@@ -565,7 +645,7 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
const url = [...pathSegments(router.prefix, params, env), ...pathSegments(route.path, params, env)]
|
||||
const request: PropertyItem[] = [
|
||||
requestItem(words.items.url, url),
|
||||
requestItem(words.items.method, [{ text: route.method, role: 'keyword', ...(METHOD_COLORS[route.method] && { color: METHOD_COLORS[route.method] }) }]),
|
||||
requestItem(words.items.method, methodText(route.method)),
|
||||
...(['path', 'query', 'header', 'cookie'] as const).flatMap(place => at(place).length > 0 ? [requestItem(words.items[place], paramsText(at(place), env))] : []),
|
||||
...(body.length > 0 ? [requestItem(words.items.body, body.length === 1 && body[0].type ? context.translate(body[0].type) : paramsText(body, env))] : []),
|
||||
]
|
||||
@@ -587,7 +667,6 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
addRaises(definition?.childForFieldName('body'), statuses, env)
|
||||
if (placed.some(item => item.place !== 'dependency'))
|
||||
statuses.add(numericStatus('422'), [hint(words.validation)], [hint(words.automatic)])
|
||||
const responses = statuses.rows()
|
||||
|
||||
const doc = facts.doc && context.doc(facts.doc)
|
||||
const tags = [...router.tags, ...listItems(call.keywords.get('tags'))].map(tag => tagText(tag, env))
|
||||
@@ -602,6 +681,7 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
const rows: PropertyRow[] = [
|
||||
...(facts.name ? [{ label: labels.name, value: [name(facts.name, 'function')] }] : []),
|
||||
...(doc ? [{ label: labels.doc, value: doc }] : []),
|
||||
{ label: labels.router, value: [name(router.target, 'variable')] },
|
||||
...(tags.length > 0 ? [{ label: labels.tags, value: joined(tags, separator) }] : []),
|
||||
{ label: labels.async, value: [word(facts.async ? words.yes : words.no)] },
|
||||
...(decorators.length > 0 ? [{ label: labels.decorators, value: joined(decorators.map(other => context.translate(other)), { text: '\n', role: 'plain' }) }] : []),
|
||||
@@ -612,14 +692,7 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
columns: words.requestHeads,
|
||||
items: request,
|
||||
},
|
||||
{
|
||||
label: labels.responses,
|
||||
value: [hint(responses.map(row => textOf(row.code)).join(' / '))],
|
||||
columns: words.responseHeads,
|
||||
items: responses.map(row => ({
|
||||
cells: [row.code, joined(row.contents, separator), joined(row.notes, { text: words.messageSeparator, role: 'comment' })],
|
||||
})),
|
||||
},
|
||||
responsesRow(labels.responses, statuses, env),
|
||||
]
|
||||
return {
|
||||
kind: KIND,
|
||||
@@ -631,20 +704,78 @@ function routeCard(block: FunctionBlock, route: Route, env: Env): RuleCard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由器卡片:名称 / 说明 / 前缀 / 标签 / 依赖 / 默认响应(有才显示),接口表列这份文件里挂在它上面的全部接口:
|
||||
* 方法 | 路径(前缀 + 装饰器里的路径,`{参数}` 讲那个函数的参数)| 函数(⌘ + 点击跳到那张接口卡片)| 说明(docstring 第一行)。
|
||||
*/
|
||||
function routerCard(block: VariableBlock, router: Router, routes: readonly RouteOn[], env: Env): RuleCard {
|
||||
const { context, words } = env
|
||||
const { labels } = words
|
||||
const separator = hint(words.listSeparator)
|
||||
const doc = block.facts.doc && context.doc(block.facts.doc)
|
||||
const tags = router.tags.map(tag => tagText(tag, env))
|
||||
const dependencies = listedDependencies(router.dependencies, env)
|
||||
const statuses = new Statuses()
|
||||
addResponses(router.responses, statuses, env)
|
||||
const rows: PropertyRow[] = [
|
||||
{ label: labels.name, value: [name(router.target, 'variable')] },
|
||||
...(doc ? [{ label: labels.doc, value: doc }] : []),
|
||||
...(router.prefix ? [{ label: labels.prefix, value: pathSegments(router.prefix, new Map(), env) }] : []),
|
||||
...(tags.length > 0 ? [{ label: labels.tags, value: joined(tags, separator) }] : []),
|
||||
...(dependencies.length > 0 ? [{ label: labels.dependencies, value: joined(dependencies, separator) }] : []),
|
||||
...(statuses.rows().length > 0 ? [responsesRow(labels.defaultResponses, statuses, env)] : []),
|
||||
{
|
||||
label: labels.routes,
|
||||
value: [hint(words.count(routes.length))],
|
||||
...(routes.length > 0 && {
|
||||
columns: words.routeHeads,
|
||||
items: routes.map(({ route, definition, name: fn }) => {
|
||||
const params = parametersOf(definition)
|
||||
const doc = docFirstLine(definition, env)
|
||||
return {
|
||||
cells: [
|
||||
methodText(route.method),
|
||||
[...pathSegments(router.prefix, params, env), ...pathSegments(route.path, params, env)],
|
||||
[name(fn, 'function')],
|
||||
doc ? [{ text: doc, role: 'comment' }] : [],
|
||||
],
|
||||
}
|
||||
}),
|
||||
}),
|
||||
},
|
||||
]
|
||||
return {
|
||||
kind: ROUTER_KIND,
|
||||
badge: { label: words.routerKind, color: ROUTER_COLOR, icon: ROUTER_ICON },
|
||||
rows,
|
||||
defines: range(router.target),
|
||||
absorbs: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const fastapiRule: LibraryRule = {
|
||||
id: RULE_ID,
|
||||
label: 'FastAPI 接口',
|
||||
languageIds: ['python'],
|
||||
libraries: ['fastapi'],
|
||||
claim: (block, context) => {
|
||||
if (block.kind !== 'function' || block.facts.decorators.length === 0)
|
||||
return undefined
|
||||
const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH, names: namesOf(context.imports) }
|
||||
const routers = routersOf(block.node.tree.rootNode, env)
|
||||
for (const decorator of block.facts.decorators) {
|
||||
const route = routeOf(decorator, routers)
|
||||
if (route)
|
||||
return routeCard(block, route, env)
|
||||
const root = block.node.tree.rootNode
|
||||
if (block.kind === 'function' && block.facts.decorators.length > 0) {
|
||||
const routers = routersOf(root, env)
|
||||
for (const decorator of block.facts.decorators) {
|
||||
const route = routeOf(decorator, routers)
|
||||
if (route)
|
||||
return routeCard(block, route, env)
|
||||
}
|
||||
}
|
||||
// 路由对象只认模块顶层的赋值(routersOf),按赋值目标的位置认出是不是这一块
|
||||
if ((block.kind === 'const' || block.kind === 'variable') && block.facts.declarators.length === 1) {
|
||||
const routers = routersOf(root, env)
|
||||
const target = block.facts.declarators[0].name
|
||||
const router = routers.get(target.text)
|
||||
if (router?.target.startIndex === target.startIndex)
|
||||
return routerCard(block, router, routesOn(root, routers, router), env)
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ListOrdered,
|
||||
Lock,
|
||||
MessageSquareText,
|
||||
Network,
|
||||
Package,
|
||||
Repeat,
|
||||
ServerCog,
|
||||
@@ -52,6 +53,7 @@ export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
|
||||
'server-cog': ServerCog,
|
||||
'calendar-clock': CalendarClock,
|
||||
'globe': Globe,
|
||||
'network': Network,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface ThemeColor { dark: string, light: string }
|
||||
export type KindIconName
|
||||
= | 'import' | 'square-function' | 'box' | 'braces' | 'list-ordered' | 'triangle-alert' | 'tag' | 'lock' | 'variable'
|
||||
| 'split' | 'corner-down-right' | 'repeat' | 'shield' | 'shield-alert' | 'shield-check' | 'corner-down-left' | 'code'
|
||||
| 'message-square-text' | 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe'
|
||||
| 'message-square-text' | 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network'
|
||||
|
||||
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
Reference in new issue
Block a user