AIDeveloper-PC/requirements_generator/utils/output_writer.py

430 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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