AIDeveloper-PC/requirements_generator/utils/output_writer.py

430 lines
14 KiB
Python
Raw Normal View History

2026-03-04 18:09:45 +00:00
# utils/output_writer.py - 代码文件 & JSON 输出工具
import os
import json
from pathlib import Path
from typing import Dict, List
import config
# 各语言文件扩展名映射
LANGUAGE_EXT_MAP: Dict[str, str] = {
"python": ".py",
"javascript": ".js",
"typescript": ".ts",
"java": ".java",
"go": ".go",
"rust": ".rs",
"cpp": ".cpp",
"c": ".c",
"csharp": ".cs",
"ruby": ".rb",
"php": ".php",
"swift": ".swift",
"kotlin": ".kt",
}
# 合法的通用类型集合
VALID_TYPES = {
"integer", "string", "boolean", "float",
"list", "dict", "object", "void", "any",
}
# 合法的 inout 值
VALID_INOUT = {"in", "out", "inout"}
def get_file_extension(language: str) -> str:
"""
获取指定语言的文件扩展名
Args:
language: 编程语言名称小写
Returns:
文件扩展名含点号 '.py'
"""
return LANGUAGE_EXT_MAP.get(language.lower(), ".txt")
def build_project_output_dir(project_name: str) -> str:
"""
构建项目输出目录路径
Args:
project_name: 项目名称
Returns:
输出目录路径
"""
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in project_name)
return os.path.join(config.OUTPUT_BASE_DIR, safe_name)
def ensure_project_dir(project_name: str) -> str:
"""
确保项目输出目录存在不存在则创建
Args:
project_name: 项目名称
Returns:
创建好的目录路径
"""
output_dir = build_project_output_dir(project_name)
os.makedirs(output_dir, exist_ok=True)
init_file = os.path.join(output_dir, "__init__.py")
if not os.path.exists(init_file):
Path(init_file).write_text(
"# Auto-generated project package\n", encoding="utf-8"
)
return output_dir
def write_code_file(
output_dir: str,
function_name: str,
language: str,
content: str,
) -> str:
"""
将代码内容写入指定目录的文件
Args:
output_dir: 输出目录路径
function_name: 函数名用于生成文件名
language: 编程语言
content: 代码内容
Returns:
写入的文件完整路径
"""
ext = get_file_extension(language)
file_name = f"{function_name}{ext}"
file_path = os.path.join(output_dir, file_name)
Path(file_path).write_text(content, encoding="utf-8")
return file_path
def write_project_readme(
output_dir: str,
project_name: str,
requirements_summary: str,
) -> str:
"""
在项目目录生成 README.md 文件
Args:
output_dir: 项目输出目录
project_name: 项目名称
requirements_summary: 功能需求摘要文本
Returns:
README.md 文件路径
"""
readme_content = f"""# {project_name}
> Auto-generated by Requirement Analyzer
## 功能需求列表
{requirements_summary}
"""
readme_path = os.path.join(output_dir, "README.md")
Path(readme_path).write_text(readme_content, encoding="utf-8")
return readme_path
# ══════════════════════════════════════════════════════
# 函数签名 JSON 导出
# ══════════════════════════════════════════════════════
def build_signatures_document(
project_name: str,
project_description: str,
signatures: List[dict],
) -> dict:
"""
将函数签名列表包装为带项目信息的顶层文档结构
Args:
project_name: 项目名称写入 "project" 字段
project_description: 项目描述写入 "description" 字段
signatures: 函数签名 dict 列表写入 "functions" 字段
Returns:
顶层文档 dict结构为::
{
"project": "<project_name>",
"description": "<project_description>",
"functions": [ ... ]
}
"""
return {
"project": project_name,
"description": project_description or "",
"functions": signatures,
}
def patch_signatures_with_url(
signatures: List[dict],
func_name_to_url: Dict[str, str],
) -> List[dict]:
"""
将代码文件的路径URL回写到对应函数签名的 "url" 字段
遍历签名列表根据 signature["name"] func_name_to_url 中查找
对应路径找到则写入 "url" 字段未找到则写入空字符串不抛出异常
"url" 字段插入位置紧跟在 "type" 字段之后以保持字段顺序的可读性::
{
"name": "create_user",
"requirement_id": "REQ.01",
"description": "...",
"type": "function",
"url": "/abs/path/to/create_user.py", 新增
"parameters": { ... },
"return": { ... }
}
Args:
signatures: 原始签名列表in-place 修改
func_name_to_url: {函数名: 代码文件绝对路径} 映射表
CodeGenerator.generate_batch() 的进度回调收集
Returns:
修改后的签名列表与传入的同一对象方便链式调用
"""
for sig in signatures:
func_name = sig.get("name", "")
url = func_name_to_url.get(func_name, "")
_insert_field_after(sig, after_key="type", new_key="url", new_value=url)
return signatures
def _insert_field_after(
d: dict,
after_key: str,
new_key: str,
new_value,
) -> None:
"""
在有序 dict 中将 new_key 插入到 after_key 之后
after_key 不存在则追加到末尾
new_key 已存在则直接更新其值不改变位置
Args:
d: 目标 dictPython 3.7+ 保证插入顺序
after_key: 参考键名
new_key: 要插入的键名
new_value: 要插入的值
"""
if new_key in d:
d[new_key] = new_value
return
items = list(d.items())
insert_pos = len(items)
for i, (k, _) in enumerate(items):
if k == after_key:
insert_pos = i + 1
break
items.insert(insert_pos, (new_key, new_value))
d.clear()
d.update(items)
def write_function_signatures_json(
output_dir: str,
signatures: List[dict],
project_name: str,
project_description: str,
file_name: str = "function_signatures.json",
) -> str:
"""
将函数签名列表连同项目信息一起导出为 JSON 文件
输出的 JSON 顶层结构为::
{
"project": "<project_name>",
"description": "<project_description>",
"functions": [
{
"name": "...",
"requirement_id": "...",
"description": "...",
"type": "function",
"url": "/abs/path/to/xxx.py",
"parameters": { ... },
"return": { ... }
},
...
]
}
Args:
output_dir: JSON 文件写入目录
signatures: 函数签名 dict 列表应已通过
patch_signatures_with_url() 写入 "url" 字段
project_name: 项目名称
project_description: 项目描述
file_name: 输出文件名默认 function_signatures.json
Returns:
写入的 JSON 文件完整路径
Raises:
OSError: 目录不可写
"""
os.makedirs(output_dir, exist_ok=True)
document = build_signatures_document(project_name, project_description, signatures)
file_path = os.path.join(output_dir, file_name)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(document, f, ensure_ascii=False, indent=2)
return file_path
# ══════════════════════════════════════════════════════
# 签名结构校验
# ══════════════════════════════════════════════════════
def validate_signature_schema(signature: dict) -> List[str]:
"""
校验单个函数签名 dict 是否符合规范
校验范围
- 顶层必填字段name / requirement_id / description / type / parameters
- 可选字段 "url"若存在则必须为非空字符串
- parameters每个参数的 type / inout / required 字段
- returntype 字段 + on_success / on_failure 子结构
- void 函数on_success / on_failure 应为 null
- void 函数on_success / on_failure 必须存在
value非空 description非空均需填写
Args:
signature: 单个函数签名 dict
Returns:
错误信息字符串列表列表为空表示校验通过
"""
errors: List[str] = []
# ── 顶层必填字段 ──────────────────────────────────
for key in ("name", "requirement_id", "description", "type", "parameters"):
if key not in signature:
errors.append(f"缺少顶层字段: '{key}'")
# ── url 字段(可选,存在时校验非空)─────────────────
if "url" in signature:
if not isinstance(signature["url"], str):
errors.append("'url' 字段必须是字符串类型")
elif signature["url"] == "":
errors.append("'url' 字段不能为空字符串(代码文件路径未成功回写)")
# ── parameters ────────────────────────────────────
params = signature.get("parameters", {})
if not isinstance(params, dict):
errors.append("'parameters' 必须是 dict 类型")
else:
for pname, pdef in params.items():
if not isinstance(pdef, dict):
errors.append(f"参数 '{pname}' 定义必须是 dict")
continue
# type支持联合类型如 "string|integer"
if "type" not in pdef:
errors.append(f"参数 '{pname}' 缺少 'type' 字段")
else:
parts = [p.strip() for p in pdef["type"].split("|")]
if not all(p in VALID_TYPES for p in parts):
errors.append(
f"参数 '{pname}' 的 type='{pdef['type']}' 含有不合法的类型"
)
# inout
if "inout" not in pdef:
errors.append(f"参数 '{pname}' 缺少 'inout' 字段")
elif pdef["inout"] not in VALID_INOUT:
errors.append(
f"参数 '{pname}' 的 inout='{pdef['inout']}' 应为 in/out/inout"
)
# required
if "required" not in pdef:
errors.append(f"参数 '{pname}' 缺少 'required' 字段")
elif not isinstance(pdef["required"], bool):
errors.append(
f"参数 '{pname}''required' 应为布尔值 true/false"
f"当前为: {pdef['required']!r}"
)
# ── return ────────────────────────────────────────
ret = signature.get("return")
if ret is None:
errors.append(
"缺少 'return' 字段void 函数请填 "
"{\"type\": \"void\", \"on_success\": null, \"on_failure\": null}"
)
elif not isinstance(ret, dict):
errors.append("'return' 必须是 dict 类型")
else:
ret_type = ret.get("type")
if not ret_type:
errors.append("'return' 缺少 'type' 字段")
elif ret_type not in VALID_TYPES:
errors.append(f"'return.type'='{ret_type}' 不在合法类型列表中")
is_void = (ret_type == "void")
for sub_key in ("on_success", "on_failure"):
sub = ret.get(sub_key)
if is_void:
if sub is not None:
errors.append(
f"void 函数的 'return.{sub_key}' 应为 null"
f"当前为: {sub!r}"
)
else:
if sub is None:
errors.append(
f"非 void 函数缺少 'return.{sub_key}'"
f"请描述{'成功' if sub_key == 'on_success' else '失败'}时的返回值"
)
elif not isinstance(sub, dict):
errors.append(f"'return.{sub_key}' 必须是 dict 类型")
else:
if "value" not in sub:
errors.append(f"'return.{sub_key}' 缺少 'value' 字段")
elif sub["value"] == "":
errors.append(
f"'return.{sub_key}.value' 不能为空字符串,"
f"请填写具体返回值、值域描述或结构示例"
)
if "description" not in sub or sub.get("description") in (None, ""):
errors.append(f"'return.{sub_key}.description' 不能为空")
return errors
def validate_all_signatures(signatures: List[dict]) -> Dict[str, List[str]]:
"""
批量校验函数签名列表
注意此函数接受的是纯签名列表即顶层文档的 "functions" 字段
而非包含 project/description 的顶层文档
Args:
signatures: 函数签名 dict 列表
Returns:
{函数名: [错误信息, ...]} 字典仅包含有错误的条目
"""
report: Dict[str, List[str]] = {}
for sig in signatures:
name = sig.get("name", f"unknown_{id(sig)}")
errs = validate_signature_schema(sig)
if errs:
report[name] = errs
return report