#!/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())