crud-grpc-ruleset

star 341

PowerX CRUD gRPC 顶层 ruleset 约束。

ArtisanCloud By ArtisanCloud schedule Updated 3/25/2026

name: crud-grpc-ruleset description: PowerX CRUD gRPC 顶层 ruleset 约束。

PowerX CRUD gRPC Ruleset

步骤

  1. 打开 本文件内嵌规则
  2. 按规则执行实现/校对。
  3. 完成后按核对清单验收。

核对点

  • 与 PowerX 当前代码结构、路径与命名一致。
  • 仅在传输层/契约层做职责内改动,不跨层越界。

规则(内嵌)

crud_grpc.yaml

kind: ruleset
name: crud_grpc
version: 1.1.0
owner: powerx
status: stable

meta:
  intent: >
    规定 CRUD 的 gRPC 契约与消息形态(proto 结构、分页/错误/鉴权/多租户语义及命名规范),
    保证与 REST/Service 层语义等价,可双向对照;并校验 go_package 与 buf.gen.yaml 前缀一致。
  references:
    - constitution.md
    - crud_api_rest.yaml
    - crud_service.yaml
    - crud_repository.yaml
    - proto_gen.yaml
    - docs/grpc/readme.md

scope:
  applies_to:
    - "api/grpc/**/*.proto"

principles:
  - 使用 proto3;每个 proto 必须声明 go_package。
  - 包与命名:package 用 `<org>.<product>.<domain>`;Service 命名 `{{Entity}}Service`。
  - RPC 五件套:Create/List/Get/Update/Delete;批量操作以 Batch 作为后缀。
  - 分页:与 REST 对齐字段(page,page_size,total,pages),允许扩展 cursor_token。
  - 错误语义:业务错误优先封装在 `common.v1.ResponseMeta`,仅在传输/系统异常时返回标准 gRPC status。
  - 多租户/鉴权:tenant/actor 从 metadata 推导,不作为业务字段传入;request_id/trace_id 透传。
  - 流式场景:仅在必要时使用 server streaming;事件名与 REST SSE 对齐。

checks:

  # ---- 基础 proto 规范 ----
  proto.basics:
    - id: proto.syntax
      level: error
      when: { glob: "api/grpc/**/*.proto" }
      assert:
        - must_contain: 'syntax = "proto3";'
        - must_contain_regex: 'option\\s+go_package\\s*=\\s*"[^\"]+";'
    - id: proto.package.naming
      level: error
      when: { glob: "api/grpc/**/*.proto" }
      assert:
        - must_contain_regex: "package [a-z0-9]+(\\.[a-z0-9_]+)+;"

  # ---- Service + RPC 五件套形态(按资源原型做匹配)----
  service_and_rpcs:
    - id: svc.crud.methods
      level: error
      when: { glob: "api/grpc/**/{{resource}}.proto" }
      assert:
        - must_contain_regex: "service\\s+{{Entity}}Service\\s*{"
        - must_contain: "rpc Create{{Entity}}(Create{{Entity}}Request) returns ({{Entity}}Response);"
        - must_contain: "rpc List{{Entity}}(List{{Entity}}Request) returns (List{{Entity}}Response);"
        - must_contain: "rpc Get{{Entity}}(Get{{Entity}}Request) returns ({{Entity}}Response);"
        - must_contain: "rpc Update{{Entity}}(Update{{Entity}}Request) returns ({{Entity}}Response);"
        - must_contain: "rpc Delete{{Entity}}(Delete{{Entity}}Request) returns (google.protobuf.Empty);"

  # ---- 消息与分页形态 ----
  messages_and_pagination:
    - id: msg.pagination.shape
      level: error
      when: { glob: "api/grpc/**/{{resource}}.proto" }
      assert:
        - must_contain: "message Pagination { int64 total = 1; int32 page = 2; int32 page_size = 3; int32 pages = 4; }"
        - must_contain_regex: "message List{{Entity}}Response\\s*{\\s*repeated {{Entity}} items = 1;\\s*Pagination pagination = 2;"

  # ---- 多租户/鉴权提示(作为规范注释)----
  auth_and_tenant:
    - id: comments.tenant.auth
      level: warn
      when: { glob: "api/grpc/**/{{resource}}.proto" }
      assert:
        - should_contain: "// NOTE: tenant/actor 从 metadata 推导,不作为业务字段传入"

  # ---- go_package 与 buf.gen.yaml 的 go_package_prefix 一致性 ----
  go_package_prefix:
    - id: proto.go_package_prefix.config.exists
      level: error
      when: { file: "api/grpc/contract/buf.gen.yaml" }
      assert:
        - must_contain: "go_package_prefix:"
        - must_contain: "default: github.com/ArtisanCloud/PowerX/api/grpc/gen"
    - id: proto.go_package_prefix.match
      level: error
      when: { glob: "api/grpc/**/*.proto" }
      assert:
        - must_contain_regex: 'option\\s+go_package\\s*=\\s*"github\\.com/ArtisanCloud/PowerX/api/grpc/gen[^"]*;[a-zA-Z0-9_]+";'
      message: "proto 的 go_package 必须以 github.com/ArtisanCloud/PowerX/api/grpc/gen 起始,并以 ;<包名> 结尾"
    - id: proto.go_package_prefix.output.dir
      level: error
      when: { file: "api/grpc/contract/buf.gen.yaml" }
      assert:
        - must_contain_regex: "out:\\s*api/grpc/gen"
        - must_contain: "opt:\n          - paths=source_relative"

acceptance:
  checklist:
    - "[ ] 使用 proto3,且每个 proto 设置 go_package"
    - "[ ] {{Entity}}Service 含 Create/List/Get/Update/Delete RPC"
    - "[ ] 列表响应包含 items + Pagination(total/page/page_size/pages)"
    - "[ ] 错误响应通过 ResponseMeta 对齐 HTTP(仅系统异常返回 gRPC status)"
    - "[ ] 多租户/鉴权通过 metadata 推导(非业务字段)"
    - "[ ] go_package 与 buf.gen.yaml 的 go_package_prefix.default 一致"
    - "[ ] 生成输出到 api/grpc/gen,并使用 paths=source_relative"

templates:
  proto_snippet: |
    syntax = "proto3";
    package powerx.media;

    option go_package = "github.com/ArtisanCloud/PowerX/api/grpc/gen/media;media";

    import "google/protobuf/empty.proto";
    import "google/protobuf/timestamp.proto";

    // NOTE: tenant/actor 从 metadata 推导,不作为业务字段传入
    message Pagination {
      int64 total = 1;
      int32 page = 2;
      int32 page_size = 3;
      int32 pages = 4;
    }

    message MediaAsset {
      string id = 1;                   // ULID/UUID
      uint64 tenant_id = 2;            // 服务端填充
      string name = 3;
      string code = 4;
      string meta_json = 5;
      int32 status = 6;
      google.protobuf.Timestamp created_at = 7;
      google.protobuf.Timestamp updated_at = 8;
    }

    message CreateMediaAssetRequest {
      string name = 1;
      string code = 2;
      string meta_json = 3;
      int32 status = 4;
    }
    message UpdateMediaAssetRequest {
      string id = 1;
      string name = 2;
      string meta_json = 3;
      int32 status = 4;
    }
    message GetMediaAssetRequest { string id = 1; }
    message DeleteMediaAssetRequest { string id = 1; }

    message ListMediaAssetRequest {
      int32 page = 1;
      int32 page_size = 2;
      string sort_by = 3;     // created_at/updated_at/id
      string sort_order = 4;  // asc/desc
      string q = 5;
      map<string,string> filters = 6;
    }

    message MediaAssetResponse { MediaAsset data = 1; }
    message ListMediaAssetResponse {
      repeated MediaAsset items = 1;
      Pagination pagination = 2;
    }

    service MediaAssetService {
      rpc CreateMediaAsset(CreateMediaAssetRequest) returns (MediaAssetResponse);
      rpc ListMediaAsset(ListMediaAssetRequest) returns (ListMediaAssetResponse);
      rpc GetMediaAsset(GetMediaAssetRequest) returns (MediaAssetResponse);
      rpc UpdateMediaAsset(UpdateMediaAssetRequest) returns (MediaAssetResponse);
      rpc DeleteMediaAsset(DeleteMediaAssetRequest) returns (google.protobuf.Empty);
    }
  handler_go: |
    func (s *FooServer) GetFoo(ctx context.Context, req *foov1.GetFooRequest) (*foov1.GetFooResponse, error) {
      tid := tenantIDFrom(ctx, req.GetCtx())
      if tid == 0 {
        return &foov1.GetFooResponse{Meta: badMeta(ctx, 400, "tenant_id required", req.GetCtx().GetRequestId())}, nil
      }
      foo, err := s.fooSvc.GetFoo(ctx, uint64(tid), req.GetId())
      if err != nil {
        if errors.Is(err, service.ErrFooNotFound) {
          return &foov1.GetFooResponse{Meta: badMeta(ctx, 404, "foo not found", req.GetCtx().GetRequestId())}, nil
        }
        return nil, status.Errorf(codes.Internal, "get foo: %v", err)
      }
      return &foov1.GetFooResponse{
        Meta: okMeta(ctx, req.GetCtx().GetRequestId()),
        Data: &foov1.GetFooData{Foo: toPBFoo(foo)},
      }, nil
    }
Install via CLI
npx skills add https://github.com/ArtisanCloud/PowerX --skill crud-grpc-ruleset
Repository Details
star Stars 341
call_split Forks 63
navigation Branch main
article Path SKILL.md
More from Creator
ArtisanCloud
ArtisanCloud Explore all skills →