一、Discovery 是什么?

Google Discovery Service 是 Google 所有 REST API 的 自描述 Schema 中心。每个 API 提供一个 JSON 文档(Discovery Document),精确描述该 API 的:

  • 所有资源层级(Resource Hierarchy)
  • 所有方法(Method)及其 HTTP verb、URL path
  • 所有参数(Parameter)的类型、位置、是否必填
  • 请求/响应体的 JSON Schema
  • 认证要求(OAuth2 Scopes)
  • 媒体上传/下载支持

二、Discovery 数据结构全貌

2.1 顶层:RestDescription

pub struct RestDescription {
    pub name: String,            // "drive"
    pub version: String,         // "v3"
    pub title: Option<String>,
    pub description: Option<String>,
    pub root_url: String,        // "https://www.googleapis.com/"
    pub service_path: String,    // "drive/v3/"
    pub schemas: HashMap<String, JsonSchema>,            // 所有类型定义
    pub resources: HashMap<String, RestResource>,        // 资源树
    pub parameters: HashMap<String, MethodParameter>,    // 全局参数
    pub auth: Option<AuthDescription>,                   // OAuth2 Scopes
}

2.2 资源与方法层级

RestDescription
  └── resources: HashMap<String, RestResource>
        └── RestResource
              ├── methods: HashMap<String, RestMethod>     # 当前资源的方法
              └── resources: HashMap<String, RestResource> # 子资源(递归)

示例:Drive API 的资源树

drive (RestDescription)
  └── files (RestResource)
        ├── methods:
        │   ├── "list"   → GET  /drive/v3/files
        │   ├── "get"    → GET  /drive/v3/files/{fileId}
        │   ├── "create" → POST /drive/v3/files
        │   ├── "update" → PATCH /drive/v3/files/{fileId}
        │   └── "delete" → DELETE /drive/v3/files/{fileId}
        └── resources:
              ├── comments (RestResource)
              │     └── methods: "list", "get", "create", ...
              └── revisions (RestResource)
                    └── methods: "list", "get", ...

2.3 RestMethod:单个 API 方法

pub struct RestMethod {
    pub id: Option<String>,                          // "drive.files.list"
    pub description: Option<String>,                 // 方法说明
    pub http_method: String,                         // GET/POST/PUT/PATCH/DELETE
    pub path: String,                                // "files/{fileId}"
    pub flat_path: Option<String>,                   // "files/{fileId}" (简化路径)
    pub parameters: HashMap<String, MethodParameter>,// 方法参数定义
    pub parameter_order: Vec<String>,                // 参数顺序
    pub request: Option<SchemaRef>,                  // 请求体 Schema 引用
    pub response: Option<SchemaRef>,                 // 响应体 Schema 引用
    pub scopes: Vec<String>,                         // 所需 OAuth2 Scopes
    pub supports_media_download: bool,               // 是否支持下载
    pub supports_media_upload: bool,                 // 是否支持上传
    pub media_upload: Option<MediaUpload>,           // 上传端点信息
}

2.4 MethodParameter:参数定义

pub struct MethodParameter {
    pub param_type: Option<String>,      // "string"|"integer"|"boolean"
    pub description: Option<String>,     // 参数说明
    pub location: Option<String>,        // "path"|"query"
    pub required: bool,                  // 是否必填
    pub format: Option<String>,          // "date-time"|"byte" 等
    pub default: Option<String>,         // 默认值
    pub enum_values: Option<Vec<String>>,// 枚举值
    pub repeated: bool,                  // 是否可重复(数组)
    pub minimum: Option<String>,
    pub maximum: Option<String>,
    pub deprecated: bool,
}

2.5 JsonSchema:请求/响应体 Schema

pub struct JsonSchema {
    pub id: Option<String>,
    pub schema_type: Option<String>,     // "object"|"array"|"string"...
    pub description: Option<String>,
    pub properties: HashMap<String, JsonSchemaProperty>, // 对象属性
    pub schema_ref: Option<String>,      // $ref 引用
    pub items: Option<Box<JsonSchemaProperty>>,          // 数组元素类型
    pub required: Vec<String>,           // 必填字段列表
}
 
pub struct JsonSchemaProperty {
    pub prop_type: Option<String>,       // 字段类型
    pub description: Option<String>,
    pub schema_ref: Option<String>,      // $ref 引用(嵌套对象)
    pub format: Option<String>,
    pub items: Option<Box<JsonSchemaProperty>>,
    pub read_only: bool,
    pub default: Option<String>,
    pub enum_values: Option<Vec<String>>,
}

三、Discovery 文档的获取与缓存

3.1 获取流程

用户输入: gws drive files list

  main.rs: parse_service_and_version()
    → 提取服务名 "drive"
    → 通过 services.rs 的 SERVICE 注册表解析:
      "drive" → api_name="drive", version="v3"
    → 支持 --api-version 覆盖版本

  discovery::fetch_discovery_document("drive", "v3")

    步骤 1: 输入验证
      → validate_api_identifier("drive")  # 只允许 [a-zA-Z0-9\-_.]
      → validate_api_identifier("v3")     # 防止路径遍历注入

    步骤 2: 检查本地缓存
      → 缓存路径: ~/.config/gws/cache/drive_v3.json
      → TTL: 24 小时
      → 缓存命中 → 直接解析返回

    步骤 3: 远程获取(缓存未命中或过期)
      → 主 URL: https://www.googleapis.com/discovery/v1/apis/drive/v3/rest
      → 备用 URL: https://drive.googleapis.com/$discovery/rest?version=v3
         (用于 Forms、Keep、Meet 等新 API)

    步骤 4: 写入缓存
      → 保存到 ~/.config/gws/cache/drive_v3.json

3.2 双层 Discovery 源

// 第一层:标准 Discovery   URL
// https://www.googleapis.com/discovery/v1/apis/{service}/{version}/rest
let url = format!(
    "https://www.googleapis.com/discovery/v1/apis/{}/{}/rest",
    service, version
);
 
// 第二层:$discovery 端点(fallback)
// https://{service}.googleapis.com/$discovery/rest?version={version}
// 用于 Forms、Keep、Meet 等使用新式 API 设置的服务
let alt_url = format!("https://{service}.googleapis.com/$discovery/rest");

3.3 安全验证

// API 标识符验证 —— 防止路径遍历和注入
pub fn validate_api_identifier(s: &str) -> Result<&str, GwsError> {
    // 只允许 [a-zA-Z0-9\-_.] 字符
    // 拒绝: 路径遍历 (../)、查询注入 (?)、片段注入 (#)、URL编码绕过 (%)
}

四、从 Discovery 到 CLI 命令树

4.1 入口流程(main.rs)

run()
  → 解析 first_arg (服务名)
  → 处理特殊命令 (schema/generate-skills/auth)
  → 对于 workflow 服务:使用空的 RestDescription(合成服务)
  → 对于其他服务:fetch_discovery_document()
  → commands::build_cli(&doc)   # 动态构建命令树
  → cli.try_get_matches_from()  # 二次解析参数
  → helpers::handle() 或 executor::execute_method()

4.2 命令树构建(commands.rs)

build_cli(doc: &RestDescription)

  步骤 1: 创建根命令 "gws"
    → 设置 about 文本(来自 doc.description)
    → 添加全局参数:
      - --sanitize     (Model Armor 模板)
      - --dry-run      (本地验证)
      - --format       (json/table/yaml/csv)
    → subcommand_required(true)

  步骤 2: 注入 Helper 命令(如果有)
    → helpers::get_helper(&doc.name)
    → helper.inject_commands(root, doc)  # 添加 +verb 命令

  步骤 3: 构建资源子命令(递归)
    → 遍历 doc.resources
    → build_resource_command("files", &doc.resources["files"])

4.3 资源命令构建(递归)

build_resource_command(name: "files", resource: &RestResource)

  步骤 1: 创建子命令 "files"
    → about: "Operations on the 'files' resource"

  步骤 2: 为每个方法创建子命令(按字母排序)
    为 "create" 方法:
      → 创建 "create" 子命令
      → about: 从 method.description 截取(CLI_DESCRIPTION_LIMIT 字符)
      → 添加标准参数:
        - --params   (URL/Query 参数的 JSON 字符串)
        - --output   (二进制响应输出路径)
      → 如果 method.request 存在:
        - 添加 --json   (请求体的 JSON 字符串)
      → 如果 method.supports_media_upload:
        - 添加 --upload              (上传文件路径)
        - 添加 --upload-content-type (MIME 类型)
      → 添加分页参数:
        - --page-all   (自动分页)
        - --page-limit (最大页数,默认 10)
        - --page-delay (页间延迟,默认 100ms)

  步骤 3: 为子资源构建命令(递归)
    → build_resource_command("comments", &resource.resources["comments"])
    → build_resource_command("revisions", &resource.resources["revisions"])

  步骤 4: 如果没有任何子命令和子资源,返回 None(不显示此命令)

4.4 两阶段 CLI 解析

这是 gws 架构的核心创新:

阶段 1: 预解析
  用户输入: gws drive files get --params '{"fileId":"abc"}'

  → 提取 first_arg = "drive"
  → 其他参数全部忽略
  → 根据 "drive" 获取 Discovery Document

阶段 2: 动态构建 + 完整解析
  → 从 Discovery Document 构建完整命令树:
    gws
      └── files
            ├── list
            ├── get    [--params, --output, --page-all, ...]
            ├── create  [--params, --json, --upload, ...]
            └── ...

  → cli.try_get_matches_from(sub_args)
    解析: files + get + --params '{"fileId":"abc"}'
  → 返回 matches,包含解析后的参数值

五、Path 模板渲染

5.1 RFC 6570 路径模板

Discovery Document 中的 path 采用 RFC 6570 URI 模板语法:

模板语法含义示例 input渲染结果
{fileId}标准展开,编码特殊字符abc/123abc%2F123
{+name}保留 / 的分段展开projects/p1/locs/usprojects/p1/locs/us

5.2 flatPath vs path 选择逻辑

// 优先使用 flatPath,但当其占位符与参数名不匹配时回退到 path
// 例如 Slides API 的 bug:
//   flatPath: "v1/presentations/{presentationsId}"  (拼写错误,复数)
//   path:     "v1/presentations/{+presentationId}"  (正确)
//   parameter: "presentationId" (单数)
let path_template = match method.flat_path {
    Some(fp) if all_path_params_match(fp, method.parameters) => fp,
    _ => method.path.as_str(),
};

5.3 参数分离逻辑

build_url(doc, method, params):

  1. 提取路径参数
    → extract_template_path_parameters(path_template)
    → 返回 {"fileId"}

  2. 分离参数
    对于 params 中的每个 (key, value):
      → 如果 key 在路径参数中 → 跳过(后续渲染)
      → 如果 key.location == "path" 但不在模板中 → 报错
      → 如果 key.repeated → 展开 JSON 数组为多个查询参数
      → 否则 → 添加到 query_params

  3. 渲染路径
    → render_path_template(path_template, params)
    → {fileId} → encode_path_segment("abc/123") → "abc%2F123"
    → {+name} → encode_path_preserving_slashes(...) → 保留斜杠

  4. 拼接完整 URL
    → 如果是上传 → 使用 media_upload.simple.path 端点
    → 否则 → base_url + 渲染后的 path

六、Schema 验证

6.1 请求体验证

executor::execute_method()
  → parse_and_validate_inputs()
    → 如果 method.request 存在:
      → validate_body_against_schema(body, schema_name, doc)
        → 递归遍历 JSON 对象
        → 检查必填字段 (schema.required)
        → 检查字段类型 (prop_type)
        → 检查未知字段
        → 检查枚举值 (enum_values)
        → 递归验证 $ref 引用
        → 验证数组子项
        → 验证嵌套对象

6.2 验证规则

fn validate_property(value, prop_schema, doc, path, errors):
  1. 如果有 $ref → 递归验证引用类型
  2. 类型检查: string/integer/number/boolean/array/object
  3. 数组子项验证
  4. 嵌套对象属性验证
  5. 枚举值验证

6.3 错误格式

验证失败时返回统一的错误格式:
{
  "error": {
    "code": 400,
    "message": "Request body failed schema validation:\n- name: Missing required property 'status'\n- count: Expected type 'integer', found string"
  }
}

七、特殊处理

7.1 合成服务(workflow)

// workflow 服务没有对应的 Discovery Document
// 使用空的 RestDescription,完全由 Helper 命令驱动
let doc = if api_name == "workflow" {
    RestDescription {
        name: "workflow".to_string(),
        description: Some("Cross-service productivity workflows".to_string()),
        ..Default::default()
    }
};

7.2 服务注册表

services.rs 维护了 18 个已知服务及其别名映射:

别名API 名称版本
drivedrivev3
gmailgmailv1
sheetssheetsv4
calendarcalendarv3
admin-reports, reportsadminreports_v1
docsdocsv1
slidesslidesv1
taskstasksv1
peoplepeoplev1
chatchatv1
classroomclassroomv1
formsformsv1
keepkeepv1
meetmeetv2
eventsworkspaceeventsv1
modelarmormodelarmorv1
workflow, wfworkflowv1
scriptscriptv1

未注册的 API 可以通过 <apiName>:<version> 语法直接访问。

八、完整数据流图

┌──────────────────────────────────────────────────────────────────────┐
│ 用户输入                                                              │
│ gws drive files get --params '{"fileId":"abc123","fields":"name,id"}'│
└──────────────────────────────┬───────────────────────────────────────┘
                               │
                        main.rs: run()
                               │
                    ┌──────────▼──────────┐
                    │ 提取 first_arg      │
                    │ = "drive"           │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ parse_service_and   │
                    │ _version()          │
                    │ "drive" → (drive,v3)│
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ fetch_discovery_    │
                    │ document("drive",   │
                    │          "v3")      │
                    │                     │
                    │ ① 验证输入          │
                    │ ② 检查缓存 (24h)    │
                    │ ③ 获取远程 Schema   │
                    │ ④ 写入本地缓存      │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ RestDescription     │
                    │ {                   │
                    │   name: "drive",    │
                    │   version: "v3",    │
                    │   root_url: "https://│
                    │     www.googleapis. │
                    │     com/",          │
                    │   schemas: {...},   │
                    │   resources: {      │
                    │     "files": {      │
                    │       methods: {    │
                    │         "get": {    │
                    │           http_method│
                    │           : "GET",  │
                    │           path:     │
                    │            "/files/ │
                    │            {fileId}"│
                    │           ...       │
                    │         }           │
                    │       }             │
                    │     }               │
                    │   }                 │
                    │ }                   │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ commands::build_cli │
                    │ (&doc)              │
                    │                     │
                    │ → 创建根命令 gws    │
                    │ → 注入 globals      │
                    │ → 注入 helpers      │
                    │ → 递归构建资源命令  │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ clap::Command 树    │
                    │ gws                 │
                    │  ├── --sanitize     │
                    │  ├── --dry-run      │
                    │  ├── --format       │
                    │  └── files          │
                    │       ├── list      │
                    │       ├── get       │
                    │       │  ├── --params│
                    │       │  └── --output│
                    │       └── create    │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ try_get_matches_from│
                    │ ("files","get",    │
                    │  "--params","{...}")│
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ resolve_method_from │
                    │ _matches()          │
                    │ → 找到目标方法 get   │
                    │ → 提取 params_json   │
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │ executor::execute   │
                    │ _method()           │
                    │                     │
                    │ ① parse_and_        │
                    │    validate_inputs  │
                    │    → 解析 --params  │
                    │    → 验证 Schema    │
                    │    → 构建 URL       │
                    │    → 分离 query     │
                    │                     │
                    │ ② build_http_request│
                    │    → 认证头         │
                    │    → 查询参数       │
                    │    → 请求体         │
                    │                     │
                    │ ③ execute HTTP      │
                    │    → 发送请求       │
                    │    → 处理响应       │
                    │    → 自动分页       │
                    │    → 格式化输出     │
                    └──────────┬──────────┘
                               │
                               ▼
                    JSON/Table/YAML/CSV 输出

九、设计亮点总结

设计决策说明
动态命令生成不硬编码任何 API 端点,完全由 Discovery Document 驱动
两阶段解析先获取 Schema 再解析参数,优雅处理动态命令树
24 小时缓存减少 Discovery API 调用,同时确保 Schema 不过时
双层 Discovery URL兼容新旧两种 Google API 端点模式
严格输入验证API 标识符、资源名、文件路径都有完整的安全验证
flatPath 降级处理 Discovery Document 的命名不一致问题
RFC 6570 兼容支持 {+var} 展开语法,保留层级路径
Schema 验证请求体在发送前验证,减少无效 API 调用
合成服务无 Discovery 的 workflow 服务使用空描述 + Helper 驱动