filter-v3.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <?php
  2. /**
  3. * 从打包后的全量 OpenAPI spec 里切出仅 /v3/* 的部分,供 openapi-generator
  4. * 生成精简的 PHP 客户端(目前只测 /api/v3 路由)。
  5. *
  6. * 用法:
  7. * php tools/filter-v3.php spec/openapi.json spec/openapi-v3.json
  8. *
  9. * 逻辑:保留 /v3/* 路径,并递归收集它们引用的 components(schemas / responses /
  10. * parameters / requestBodies / examples / headers / securitySchemes / links /
  11. * callbacks),其余组件与路径一律丢弃。
  12. */
  13. if (PHP_SAPI !== 'cli') {
  14. fwrite(STDERR, "只允许 CLI 运行\n");
  15. exit(2);
  16. }
  17. [$script, $in, $out] = array_pad($argv, 3, null);
  18. if (!$in || !$out || !is_file($in)) {
  19. fwrite(STDERR, "用法: php {$script} <in.json> <out.json>\n");
  20. exit(2);
  21. }
  22. $spec = json_decode(file_get_contents($in), true);
  23. if (!is_array($spec) || !isset($spec['paths'])) {
  24. fwrite(STDERR, "无法解析 {$in}\n");
  25. exit(2);
  26. }
  27. /** 递归收集节点里所有指向 #/components/ 的 $ref */
  28. function collectComponentRefs(mixed $node, array &$refs): void
  29. {
  30. if (!is_array($node)) {
  31. return;
  32. }
  33. if (isset($node['$ref']) && is_string($node['$ref']) && str_starts_with($node['$ref'], '#/components/')) {
  34. $refs[$node['$ref']] = true;
  35. // $ref 节点本身不再展开(其内容在 components 里,会被下一轮收集)
  36. return;
  37. }
  38. foreach ($node as $value) {
  39. collectComponentRefs($value, $refs);
  40. }
  41. }
  42. /** 按 "#/components/{category}/{name}" 取节点 */
  43. function resolveComponent(array $spec, string $ref): mixed
  44. {
  45. $parts = explode('/', ltrim($ref, '#/'));
  46. // ['components', category, name]
  47. return $spec[$parts[0]][$parts[1]][$parts[2]] ?? null;
  48. }
  49. // 1) 选出 v3 路径
  50. $v3Paths = [];
  51. foreach ($spec['paths'] as $path => $item) {
  52. if (str_starts_with((string) $path, '/v3/')) {
  53. $v3Paths[$path] = $item;
  54. }
  55. }
  56. if ($v3Paths === []) {
  57. fwrite(STDERR, "spec 里没有 /v3/* 路径\n");
  58. exit(2);
  59. }
  60. // 2) 修正鉴权语义:源 spec 用全局 security 把全部路径标成 bearerAuth,
  61. // 但 v3 实际只有 /v3/me 与 /v3/me/* 挂 auth.v3。这里逐 operation 写准,并清掉全局 security。
  62. // 注意 /v3/me 本身(不带斜杠)也要算进去,否则生成的客户端不带 token。
  63. foreach ($v3Paths as $path => &$item) {
  64. foreach ($item as $method => &$op) {
  65. if (!is_array($op) || strtolower($method) === 'parameters') {
  66. continue;
  67. }
  68. $op['security'] = ($path === '/v3/me' || str_starts_with((string) $path, '/v3/me/'))
  69. ? [['bearerAuth' => []]]
  70. : [];
  71. }
  72. unset($op);
  73. }
  74. unset($item);
  75. // 3) 递归收集引用的组件(含 security scheme)
  76. $refs = ['#/components/securitySchemes/bearerAuth' => true];
  77. collectComponentRefs($v3Paths, $refs);
  78. do {
  79. $before = count($refs);
  80. foreach (array_keys($refs) as $ref) {
  81. $node = resolveComponent($spec, $ref);
  82. if (is_array($node)) {
  83. collectComponentRefs($node, $refs);
  84. }
  85. }
  86. } while (count($refs) !== $before);
  87. // 4) 只保留被引用的组件(保留原始分类顺序)
  88. $categories = ['schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers', 'securitySchemes', 'links', 'callbacks'];
  89. $components = $spec['components'] ?? [];
  90. $keptComponents = [];
  91. foreach ($categories as $cat) {
  92. if (!isset($components[$cat])) {
  93. continue;
  94. }
  95. foreach ($components[$cat] as $name => $node) {
  96. $ref = "#/components/{$cat}/{$name}";
  97. if (isset($refs[$ref])) {
  98. $keptComponents[$cat][$name] = $node;
  99. }
  100. }
  101. }
  102. $result = [];
  103. foreach (['openapi', 'info', 'servers', 'tags', 'externalDocs'] as $key) {
  104. if (isset($spec[$key])) {
  105. $result[$key] = $spec[$key];
  106. }
  107. }
  108. $result['components'] = $keptComponents;
  109. $result['paths'] = $v3Paths;
  110. // 5) 修掉源 spec 的数组缺 items 问题:generate.php 给 query/body 参数只写了
  111. // `type: array` 没写 `items`(严格校验会拒绝)。v3 里的这类参数都是字符串数组
  112. // (page_refs / related_id),补成 string items。
  113. normalizeArraySchemas($result);
  114. // 6) 修掉「spec 与实现不一致」的 3 个端点(搜索/建议/升级):实现仍返回
  115. // {success, data:对象} 或 {data:对象},而 spec 写成了 {data:数组, meta}。
  116. // 按现实修正 200 响应 schema,否则生成的客户端反序列化会抛
  117. // "Invalid array 'object[]'"。根因在控制器还没迁到 v3 Resource 信封,
  118. // 正确的长期修法在 api-v13 侧,这里只是让测试客户端能用。
  119. applyDriftPatches($result);
  120. file_put_contents($out, json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
  121. fwrite(STDOUT, 'v3 paths: ' . count($v3Paths) . ', components refs: ' . count($refs) . " -> {$out}\n");
  122. /** 递归给缺 items 的 `type: array` schema 补 string items */
  123. function normalizeArraySchemas(mixed &$node): void
  124. {
  125. if (! is_array($node)) {
  126. return;
  127. }
  128. if (($node['type'] ?? null) === 'array' && ! isset($node['items'])) {
  129. $node['items'] = ['type' => 'string'];
  130. }
  131. foreach ($node as &$value) {
  132. normalizeArraySchemas($value);
  133. }
  134. unset($value);
  135. }
  136. /** 修正 search / search-suggest / upgrade 的 200 响应 schema,使其与实现一致 */
  137. function applyDriftPatches(array &$spec): void
  138. {
  139. $object = ['type' => 'object'];
  140. $dataObject = ['type' => 'object', 'properties' => ['data' => $object]];
  141. $successData = ['type' => 'object', 'properties' => ['success' => ['type' => 'boolean'], 'data' => $object]];
  142. foreach (['get', 'post'] as $method) {
  143. if (isset($spec['paths']['/v3/search'][$method]['responses']['200']['content']['application/json']['schema'])) {
  144. $spec['paths']['/v3/search'][$method]['responses']['200']['content']['application/json']['schema'] = $successData;
  145. }
  146. }
  147. if (isset($spec['paths']['/v3/search-suggest']['get']['responses']['200']['content']['application/json']['schema'])) {
  148. $spec['paths']['/v3/search-suggest']['get']['responses']['200']['content']['application/json']['schema'] = $successData;
  149. }
  150. if (isset($spec['paths']['/v3/upgrade']['get']['responses']['200']['content']['application/json']['schema'])) {
  151. $spec['paths']['/v3/upgrade']['get']['responses']['200']['content']['application/json']['schema'] = $dataObject;
  152. }
  153. }