✨ feat(prototype): 代码阅读原型加 FastAPI 路由器卡片(前缀、标签、挂在上面的接口表),接口卡片写所属路由器
This commit is contained in:
1 parent
8399f0d659
commit
25569fa506
3 files changed
+49
-6
No files matched your search
@@ -26,7 +26,7 @@
|
||||
| `Prototype / 库规则 · SQLAlchemy 数据表` | luffy 的 `backend/src/luffy_agent/referral/models.py` 里的 `ReferralRelation` | 库规则(文件导入了 sqlalchemy 才启用)把模型类整块换成「数据表」卡片:名称 / 表名 / 说明 / 继承,字段表(字段 / 类型 / 约束 / 说明)和索引表;`__tablename__`、`__table_args__`、各字段块并进卡片 |
|
||||
| `Prototype / 枚举` | luffy 的 `referral/models.py` 里的 `CommissionStatus` | 枚举卡片:名称 / 访问级别 / 继承,成员收成一张表(成员 / 值 / 说明),说明取行尾注释;Python 用库规则(导入了 enum 才启用),TypeScript 的 enum 由语言插件报结构,画成同一张卡片 |
|
||||
| `Prototype / 库规则 · Celery` | luffy 的 `backend/src/luffy_agent/celery_app.py` | 库规则(导入了 celery 才启用)两张卡片:「Celery 应用」名称 / 应用名 / 消息队列 / 结果存储;「定时任务」应用 + 任务表(任务 / 频率 / 说明),频率换算成「每 5 分钟」「每天」,说明取行尾注释,`**` 展开的另起一行 |
|
||||
| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `update_user` | 库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 |
|
||||
| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `router` 和 `update_user` | 路由器(`APIRouter(...)`)和接口是两张卡片。「路由器」卡片:名称 / 前缀 / 标签(写了依赖、默认响应再加行),接口表列这个文件里挂在它上面的全部接口(方法 / 完整路径 / 函数 / 说明),函数名 Cmd+单击跳到接口卡片。库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 路由器 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 |
|
||||
| `Prototype / 块种类图标` | — | 每种块一个图标、颜色、名称的图例,库规则带的种类后面淡色写规则名;最上面一块「返回」停着鼠标、图标下面弹出名称提示 |
|
||||
| `Prototype / 遍历 · 推导式` | luffy 邀请码工具的 `"".join(secrets.choice(ALPHABET) for _ in range(length))`,外加带条件的列表、字典推导 | 写在表达式里的遍历原地套一个带循环图标和结果类型(生成器 / 列表 / 字典)的小框,框里按读的顺序重排:「遍历 range(length) 的每个 _,仅当 …,取 …」 |
|
||||
| `Prototype / 侧栏 · 复制菜单` | 同上 | 悬停行行尾第一个按钮「复制」(带小箭头),点开一组复制方式:文件名 / 文件名(不带后缀)/ 相对路径 / 绝对路径,右边淡色预览会复制出什么;文件夹没有「不带后缀」 |
|
||||
@@ -69,7 +69,7 @@
|
||||
- `Reader / TableCard`:SQLAlchemy 规则认出的数据表卡片。
|
||||
- `Reader / EnumCard`:枚举卡片。
|
||||
- `Reader / CeleryAppCard`、`Reader / BeatCard`:Celery 规则的两张卡片。
|
||||
- `Reader / RouteCard`:FastAPI 规则的接口卡片。
|
||||
- `Reader / RouterCard`、`Reader / RouteCard`:FastAPI 规则的路由器卡片和接口卡片。
|
||||
- `Reader / IterationBlock`:`Sample=generator | list | dict`,三种推导式的返回块。
|
||||
- `Reader / FilterChip`:`State=default | active`,筛选栏里一个属性的下拉框,各筛选栏共用。
|
||||
- `Reader / FilterBar`:`Active=none | access | kind`,预览顶部的筛选栏,打开的下拉菜单是勾选列表,每个值后面是块数。
|
||||
|
||||
+39
-2
@@ -907,6 +907,7 @@ const LUCIDE = {
|
||||
"server-cog": [["path", { d: "m10.852 14.772-.383.923" }], ["path", { d: "M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923" }], ["path", { d: "m13.148 9.228.383-.923" }], ["path", { d: "m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544" }], ["path", { d: "m14.772 10.852.923-.383" }], ["path", { d: "m14.772 13.148.923.383" }], ["path", { d: "M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5" }], ["path", { d: "M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5" }], ["path", { d: "M6 18h.01" }], ["path", { d: "M6 6h.01" }], ["path", { d: "m9.228 10.852-.923-.383" }], ["path", { d: "m9.228 13.148-.923.383" }]],
|
||||
"calendar-clock": [["path", { d: "M16 14v2.2l1.6 1" }], ["path", { d: "M16 2v4" }], ["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5" }], ["path", { d: "M3 10h5" }], ["path", { d: "M8 2v4" }], ["circle", { cx: "16", cy: "16", r: "6" }]],
|
||||
"globe": [["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }], ["path", { d: "M2 12h20" }]],
|
||||
"network": [["rect", { x: "16", y: "16", width: "6", height: "6", rx: "1" }], ["rect", { x: "2", y: "16", width: "6", height: "6", rx: "1" }], ["rect", { x: "9", y: "2", width: "6", height: "6", rx: "1" }], ["path", { d: "M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" }], ["path", { d: "M12 12V8" }]],
|
||||
"package": [["path", { d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }], ["path", { d: "M12 22V12" }], ["polyline", { points: "3.29 7 12 12 20.71 7" }], ["path", { d: "m7.5 4.27 9 5.15" }]],
|
||||
"git-pull-request": [["circle", { cx: "18", cy: "18", r: "3" }], ["circle", { cx: "6", cy: "6", r: "3" }], ["path", { d: "M13 6h3a2 2 0 0 1 2 2v7" }], ["path", { d: "M6 9v12" }]],
|
||||
"files": [["path", { d: "M20 7h-3a2 2 0 0 1-2-2V2" }], ["path", { d: "M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z" }], ["path", { d: "M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8" }]],
|
||||
@@ -1130,7 +1131,38 @@ function beatCard(ctx) {
|
||||
// 有参数或请求体时 FastAPI 自动带 422。
|
||||
// 挂到应用上时再叠的前缀(include_router、settings.API_V1_PREFIX)在别的文件里,URL 不算。
|
||||
// 示例:luffy 的 user/routes.py 的 update_user;Forbidden 在 core/exceptions/schemas.py,status_code = 403。
|
||||
//
|
||||
// 路由器(`X = APIRouter(...)` / `FastAPI(...)`)和挂在它上面的接口是两回事,单独一张「路由器」卡片:
|
||||
// 名称、前缀、标签、依赖(dependencies=)、默认响应(responses=,写了才有),接口表列这个文件里挂在它上面的全部接口(方法 / 路径 / 函数 / 说明),
|
||||
// 路径是完整的(前缀 + 装饰器里的路径),函数名 Cmd+单击跳到那张接口卡片。接口卡片多一行「路由器」写挂在哪个上面。
|
||||
// 接口多一个筛选属性「请求方法」(GET / POST / …),由 FastAPI 规则报。
|
||||
const ROUTE_KIND = { label: "接口", color: "#61afef", icon: "globe", rule: "FastAPI" };
|
||||
const ROUTER_KIND = { label: "路由器", color: "#7aa2f7", icon: "network", rule: "FastAPI" };
|
||||
const ROUTER_SAMPLE = {
|
||||
name: "router", prefix: "/users", tags: ["用户管理"],
|
||||
routes: [
|
||||
["GET", "/users/{user_id}", "get_user", "获取用户详情。"],
|
||||
["PUT", "/users/{user_id}", "update_user", "更新用户信息。"],
|
||||
["POST", "/users/me/avatar", "upload_current_user_avatar", "上传当前登录用户头像。"],
|
||||
],
|
||||
};
|
||||
|
||||
/** Reader / RouterCard — State=preview:FastAPI 规则认出的路由器卡片,接口表列挂在它上面的接口。 */
|
||||
function routerCard(ctx) {
|
||||
const router = ROUTER_SAMPLE;
|
||||
const { node, content } = cardShell(ctx, "State=preview", ROUTER_KIND, { kids: false });
|
||||
propRow(ctx, content, "名称", [{ text: router.name, color: COLORS.tokenVariable }]);
|
||||
propRow(ctx, content, "前缀", pathSegments(router.prefix));
|
||||
propRow(ctx, content, "标签", [{ text: router.tags.join("、"), color: COLORS.tokenString }]);
|
||||
propRow(ctx, content, "接口", countSegments(router.routes), { twisty: true });
|
||||
propTable(ctx, content, "Routes", [["方法", 80], ["路径", 240], ["函数", 260], ["说明", 0]], router.routes.map(([method, path, fn, doc]) => [`Route / ${fn}`, [
|
||||
[{ text: method, color: HTTP_METHOD_COLORS[method] }],
|
||||
pathSegments(path),
|
||||
[{ text: fn, color: COLORS.tokenFunction }],
|
||||
[{ text: doc, color: COLORS.comment }],
|
||||
]]));
|
||||
return node;
|
||||
}
|
||||
const DATACLASS_KIND = { label: "数据类", color: "#ef7fb0", icon: "package", rule: "dataclasses" };
|
||||
// Swagger UI 的方法配色,看接口文档的人最熟
|
||||
const HTTP_METHOD_COLORS = { GET: "#61affe", POST: "#49cc90", PUT: "#fca130", PATCH: "#50e3c2", DELETE: "#f93e3e" };
|
||||
@@ -1165,6 +1197,7 @@ function routeCard(ctx) {
|
||||
const { node, content } = cardShell(ctx, "State=preview", ROUTE_KIND, { kids: true });
|
||||
propRow(ctx, content, "名称", [{ text: route.name, color: COLORS.tokenFunction }]);
|
||||
propRow(ctx, content, "说明", [{ text: route.doc, color: COLORS.comment }]);
|
||||
propRow(ctx, content, "路由器", [{ text: ROUTER_SAMPLE.name, color: COLORS.tokenVariable }]);
|
||||
propRow(ctx, content, "标签", [{ text: route.tags.join("、"), color: COLORS.tokenString }]);
|
||||
propRow(ctx, content, "异步", yesNo(route.async));
|
||||
propRow(ctx, content, "依赖", route.deps.flatMap(([param, fn], index) => [
|
||||
@@ -1955,6 +1988,7 @@ function createComponents(ctx, page) {
|
||||
lib.enumCard = make("Reader / EnumCard", ["preview"], 1, () => enumCard(ctx));
|
||||
lib.celeryApp = make("Reader / CeleryAppCard", ["preview"], 1, () => celeryAppCard(ctx));
|
||||
lib.beat = make("Reader / BeatCard", ["preview"], 1, () => beatCard(ctx));
|
||||
lib.router = make("Reader / RouterCard", ["preview"], 1, () => routerCard(ctx));
|
||||
lib.route = make("Reader / RouteCard", ["preview"], 1, () => routeCard(ctx));
|
||||
lib.iteration = make("Reader / IterationBlock", ITERATION_SAMPLES.map((sample) => sample.key), 3,
|
||||
(key) => iterationBlock(ctx, ITERATION_SAMPLES.find((sample) => sample.key === key)));
|
||||
@@ -2264,7 +2298,7 @@ function kindLegend(ctx, page, lib) {
|
||||
tip.layoutPositioning = "ABSOLUTE";
|
||||
tip.x = 26;
|
||||
tip.y = 34;
|
||||
const rule = [TABLE_KIND, DATACLASS_KIND, CELERY_APP_KIND, BEAT_KIND, ROUTE_KIND];
|
||||
const rule = [TABLE_KIND, DATACLASS_KIND, CELERY_APP_KIND, BEAT_KIND, ROUTER_KIND, ROUTE_KIND];
|
||||
for (const spec of [...Object.values(BLOCK_KINDS), ...EXTRA_KINDS, ...rule]) {
|
||||
const row = filled(ctx, board, `Kind row / ${spec.label}`, { dir: "HORIZONTAL", gap: 10, align: "CENTER" });
|
||||
kindBadge(ctx, row, spec);
|
||||
@@ -2345,6 +2379,9 @@ async function build(api, stage = () => {}) {
|
||||
}
|
||||
const routeBoard = frame(ctx, "Prototype / 库规则 · FastAPI 接口", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor });
|
||||
page.appendChild(routeBoard);
|
||||
const routerInstance = lib.router.get("preview").createInstance();
|
||||
routerInstance.name = `Block / ${ROUTER_SAMPLE.name}`;
|
||||
append(routeBoard, routerInstance, true);
|
||||
const routeInstance = lib.route.get("preview").createInstance();
|
||||
routeInstance.name = `Block / ${ROUTE_SAMPLE.name}`;
|
||||
append(routeBoard, routeInstance, true);
|
||||
@@ -2469,6 +2506,6 @@ async function run(api) {
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = { ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
|
||||
module.exports = { ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
|
||||
}
|
||||
if (typeof figma !== "undefined") void run(figma);
|
||||
+8
-2
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
|
||||
const { ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
|
||||
const { fakeFigma } = require("../../figma-lab/fake-figma");
|
||||
|
||||
function action(node) { return node.reactions[0].actions[0]; }
|
||||
@@ -35,6 +35,7 @@ const EXPECTED_SETS = [
|
||||
["Reader / EnumCard", 1],
|
||||
["Reader / CeleryAppCard", 1],
|
||||
["Reader / BeatCard", 1],
|
||||
["Reader / RouterCard", 1],
|
||||
["Reader / RouteCard", 1],
|
||||
["Reader / IterationBlock", 3],
|
||||
["Reader / FilterChip", 2],
|
||||
@@ -432,8 +433,13 @@ test("the Celery rule draws the app and the beat schedule", async () => {
|
||||
test("the FastAPI rule turns a route into a card with a request table and a status-code table", async () => {
|
||||
const api = fakeFigma();
|
||||
const { routeBoard, components } = await build(api);
|
||||
assert.equal(routeBoard.children.length, 1);
|
||||
assert.equal(routeBoard.children.length, 2);
|
||||
// 路由器和接口是两张卡片:路由器的接口表列挂在它上面的全部接口,接口卡片写挂在哪个路由器上
|
||||
const router = components.lib.router.get("preview");
|
||||
assert.ok(named(router, "Kind / 路由器"));
|
||||
assert.deepEqual(router.findAll((node) => node.name.startsWith("Route / ")).map((node) => node.name.slice(8)), ROUTER_SAMPLE.routes.map(([, , fn]) => fn));
|
||||
const card = components.lib.route.get("preview");
|
||||
assert.equal(named(named(card, "Prop / 路由器"), "Value").characters, ROUTER_SAMPLE.name);
|
||||
assert.equal(named(named(card, "Prop / 依赖"), "Value").characters, "get_db(db)、get_current_user(current_user)");
|
||||
// 请求:一项一行
|
||||
const cell = (row, column) => named(named(named(card, row), column), "Text").characters;
|
||||
|
||||
Reference in new issue
Block a user