131 lines
6.1 KiB
Python
131 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""CRUD 定义(json/*.json)规范自检(QC 退回意见 #4/#5/#6 的 CI 化)。
|
||
|
||
三条硬规则(依据 crud-definition-spec / module-development-spec):
|
||
1. **合法键**:`params` 只允许规范定义的键。`order_by` / `page_size` 是**自创键**——
|
||
排序用 `sortby`(数组),分页由 sqlor `sqlPaging()` 用 ns 的 `page`/`rows` 处理,
|
||
在 CRUD JSON 里硬编码 ORDER BY / 页大小属禁项(且与 sortby 语义冲突)。
|
||
2. **alias 唯一**:alias 只用于「同一张表的多个逻辑视图」。两张**不同**的表共用同一
|
||
alias,xls2ui 会生成到同一个 `wwwroot/{alias}/` 目录互相覆盖。
|
||
3. **文件名**:`json/{table_name}.json` 或 `{alias}.json`。文件名与 tblname/alias 都不
|
||
匹配时,人和工具都对不上号(曾出现 evidence_pbl_artifact.json 这种两头不靠的命名)。
|
||
|
||
退出码 0=PASS,1=FAIL。不连库、无第三方依赖,可直接放 CI。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||
JSON_DIR = os.path.join(ROOT, 'json')
|
||
|
||
# crud-definition-spec 中 list/tree 视图 params 的合法键集合
|
||
ALLOWED_PARAM_KEYS = {
|
||
'sortby', 'logined_userorgid', 'logined_userid', 'data_filter',
|
||
'confidential_fields', 'editor', 'browserfields', 'editexclouded',
|
||
'edit_exclouded_fields', 'subtables', 'idField', 'textField',
|
||
'parentField', 'editable', 'uitype',
|
||
# editable 三 URL 规范上要求「params 顶层」(xls2ddl 从顶层读),同时兼容
|
||
# 旧写法嵌在 editable 内 —— 两处都允许,但顶层必须存在(见 TOP_LEVEL_URL_KEYS)
|
||
'new_data_url', 'update_data_url', 'delete_data_url',
|
||
# 展示/检索辅助键(规范内已文档化的可选项)
|
||
'filter_labels', 'filter_label', 'filter_title', 'data_url',
|
||
}
|
||
FORBIDDEN_PARAM_KEYS = {'order_by', 'page_size', 'page', 'rows', 'limit', 'offset',
|
||
'fields', 'joins', 'select_fields', 'grid', 'form', 'components'}
|
||
TOP_LEVEL_URL_KEYS = ('new_data_url', 'update_data_url', 'delete_data_url')
|
||
|
||
|
||
def main():
|
||
problems = []
|
||
alias_map = {}
|
||
if not os.path.isdir(JSON_DIR):
|
||
print('[pbl_evidence] no json/ directory')
|
||
return 1
|
||
|
||
for fn in sorted(os.listdir(JSON_DIR)):
|
||
if not fn.endswith('.json'):
|
||
continue
|
||
rel = 'json/' + fn
|
||
path = os.path.join(JSON_DIR, fn)
|
||
try:
|
||
d = json.loads(io.open(path, encoding='utf-8').read())
|
||
except Exception as exc:
|
||
problems.append('%s 不是合法 JSON:%s' % (rel, exc))
|
||
continue
|
||
|
||
tbl = d.get('tblname')
|
||
if not tbl:
|
||
problems.append('%s 缺 tblname(禁 tablename/name/type)' % rel)
|
||
alias = d.get('alias')
|
||
if alias:
|
||
alias_map.setdefault(alias, set()).add(tbl or rel)
|
||
|
||
# 规则 3:文件名必须等于 {tblname}.json 或 {alias}.json
|
||
stem = fn[:-5]
|
||
allowed_names = {tbl, alias} - {None}
|
||
# 多逻辑视图约定 alias = {table}_{view},故也接受以 tbl 开头的下划线名
|
||
if stem not in allowed_names and not any(
|
||
stem == t or stem.startswith(t + '_') for t in allowed_names):
|
||
problems.append('%s 文件名与 tblname=%s / alias=%s 均不匹配'
|
||
'(应为 {tblname}.json 或 {alias}.json)' % (rel, tbl, alias))
|
||
|
||
params = d.get('params')
|
||
if not isinstance(params, dict) or not params:
|
||
problems.append('%s 缺 params 或为空' % rel)
|
||
continue
|
||
|
||
# 规则 1:合法键 / 禁键
|
||
for key in params:
|
||
if key in FORBIDDEN_PARAM_KEYS:
|
||
problems.append('%s params.%s 是自创/禁用键(排序用 sortby 数组,'
|
||
'分页由 sqlor sqlPaging 用 ns.page/rows 处理)' % (rel, key))
|
||
elif key not in ALLOWED_PARAM_KEYS:
|
||
problems.append('%s params.%s 不在 crud-definition-spec 合法键内' % (rel, key))
|
||
|
||
# sortby 必须是数组
|
||
if 'sortby' in params and not isinstance(params['sortby'], list):
|
||
problems.append('%s params.sortby 必须是数组' % rel)
|
||
|
||
# editable:顶层三 URL 必须齐(xls2ddl 从 params 顶层读,嵌套写法会被静默忽略)
|
||
for key in TOP_LEVEL_URL_KEYS:
|
||
if key not in params:
|
||
problems.append('%s params 顶层缺 %s(嵌套在 editable 内会被 xls2ui 静默忽略,'
|
||
'生成默认 add_{table}.dspy)' % (rel, key))
|
||
ed = params.get('editable')
|
||
if isinstance(ed, dict):
|
||
for key in TOP_LEVEL_URL_KEYS:
|
||
if key not in ed:
|
||
problems.append('%s params.editable 缺 %s' % (rel, key))
|
||
|
||
# browserfields.alters 的 uitype=code 项必须有 data 或 dataurl
|
||
bf = params.get('browserfields') or {}
|
||
for fname, spec in (bf.get('alters') or {}).items():
|
||
if not isinstance(spec, dict):
|
||
problems.append('%s alters.%s 不是对象' % (rel, fname))
|
||
continue
|
||
if spec.get('uitype') != 'code':
|
||
problems.append('%s alters.%s uitype 应为 "code"' % (rel, fname))
|
||
if not spec.get('data') and not spec.get('dataurl'):
|
||
problems.append('%s alters.%s 既无 data 也无 dataurl' % (rel, fname))
|
||
if spec.get('data_field'):
|
||
problems.append('%s alters.%s 用了已废弃的 data_field' % (rel, fname))
|
||
|
||
# 规则 2:alias 不得被两张不同的表共用
|
||
for alias, tbls in sorted(alias_map.items()):
|
||
if len(tbls) > 1:
|
||
problems.append('alias "%s" 被多张表共用 %s —— xls2ui 会生成到同一 '
|
||
'wwwroot/%s/ 目录互相覆盖' % (alias, sorted(tbls), alias))
|
||
|
||
for line in problems:
|
||
print('FAIL ' + line)
|
||
print('[pbl_evidence] crud json: %s (%d 项问题)'
|
||
% ('PASS' if not problems else 'FAIL', len(problems)))
|
||
return 0 if not problems else 1
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|