项目文件夹

文件
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

687 行
25 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package drive
import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)
// driveMemberAddIDTypes covers every user-facing --member-type value accepted
// by the shortcut. Some values are normalized before hitting the API.
var driveMemberAddIDTypes = []string{
"email", "openid", "unionid", "openchat", "opendepartmentid",
"groupid", "appid", "wikispaceid",
}
var driveMemberAddPerms = []string{"view", "edit", "full_access"}
var driveMemberAddPermTypes = []string{"container", "single_page"}
var driveMemberAddWikiSpaceMemberKinds = []string{"wiki_space_member", "wiki_space_viewer", "wiki_space_editor"}
// driveMemberAddPrefixToType maps ID prefixes to their expected member_type
// for conflict validation when --member-type is provided explicitly.
var driveMemberAddPrefixToType = map[string]string{
"ou_": "openid",
"on_": "unionid",
"oc_": "openchat",
"od_": "opendepartmentid",
}
var driveMemberAddURLPathToType = []struct {
Prefix string
Type string
}{
{"/drive/folder/", "folder"},
{"/docx/", "docx"},
{"/doc/", "doc"},
{"/sheets/", "sheet"},
{"/base/", "bitable"},
{"/bitable/", "bitable"},
{"/wiki/", "wiki"},
{"/file/", "file"},
{"/mindnotes/", "mindnote"},
{"/slides/", "slides"},
{"/minutes/", "minutes"},
}
var driveMemberAddResourceTypes = []string{"docx", "doc", "sheet", "bitable", "file", "folder", "wiki", "mindnote", "slides", "minutes"}
const driveMemberAddBatchLimit = 10
// DriveMemberAdd adds a collaborator/member permission to a Drive resource.
var DriveMemberAdd = common.Shortcut{
Service: "drive",
Command: "+member-add",
Description: "Add a collaborator/member permission to a Drive document, file, folder, or wiki node",
Risk: "high-risk-write",
Scopes: []string{"docs:permission.member:create"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "token", Desc: "target token or document URL; type is auto-inferred from URL path when omitted", Required: true},
{Name: "type", Desc: "target resource type; required when --token is a bare token"},
{Name: "member-id", Desc: "collaborator ID; comma-separated for batch (max 10). Interpretation is decided by --member-type", Required: true},
{Name: "member-type", Desc: "ID type for --member-id; supported: email|openid|unionid|openchat|opendepartmentid|groupid|appid|wikispaceid", Required: true},
{Name: "member-kind", Desc: "request body type when --member-type=wikispaceid; one of wiki_space_member|wiki_space_viewer|wiki_space_editor"},
{Name: "perm", Desc: "permission role to grant; defaults to view"},
{Name: "perm-type", Desc: "wiki permission scope; defaults to container; rejected for non-wiki types"},
{Name: "need-notification", Type: "bool", Desc: "send an in-app notification after the grant (user identity only)"},
},
Tips: []string{
"Resource type is auto-inferred from URL paths; pass --type when --token is a bare token.",
"Supported --member-type values: email, openid, unionid, openchat, opendepartmentid, groupid, appid, wikispaceid.",
"When --member-type=wikispaceid, pass --member-kind wiki_space_member, wiki_space_viewer, or wiki_space_editor.",
"--member-type is required; if the ID prefix conflicts with --member-type (e.g. ou_xxx with email), the command rejects it.",
"--perm defaults to view (safest); use --dry-run first when granting edit or full_access.",
"For wiki nodes, --perm-type defaults to container (current page and sub-pages), except --member-type=wikispaceid where --member-kind provides the wiki-space role.",
"Department collaborator (--member-type=opendepartmentid) requires --as user; bot identity is not supported for department authorization.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := readDriveMemberAddSpec(runtime)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
spec, err := readDriveMemberAddSpec(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
return buildDriveMemberAddDryRun(spec)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
spec, err := readDriveMemberAddSpec(runtime)
if err != nil {
return err
}
if len(spec.MemberIDs) == 1 {
return executeDriveMemberAddSingle(runtime, spec)
}
return executeDriveMemberAddBatch(runtime, spec)
},
}
// driveMemberAddSpec is the normalized request model shared by Validate,
// DryRun, Execute, and output shaping so they all observe the same defaults.
type driveMemberAddSpec struct {
Token string
ResourceType string
MemberIDs []string
MemberType string
// MemberKind is the explicit --member-kind value for member_type=wikispaceid.
MemberKind string
Perm string
PermType string
NeedNotification bool
NotificationSet bool
}
// DryRunParams builds the preview query string while preserving the semantic
// difference between an omitted notification flag and an explicit false.
func (spec driveMemberAddSpec) DryRunParams() map[string]interface{} {
params := map[string]interface{}{"type": spec.ResourceType}
if spec.NotificationSet {
params["need_notification"] = spec.NeedNotification
}
return params
}
// APIQueryParams builds the query params for permission.members.create.
func (spec driveMemberAddSpec) APIQueryParams() map[string]interface{} {
params := map[string]interface{}{"type": spec.ResourceType}
if spec.NotificationSet {
params["need_notification"] = strconv.FormatBool(spec.NeedNotification)
}
return params
}
// buildMemberBody builds a single member object for the request body.
func buildMemberBody(memberID, memberType, wikiSpaceMemberKind, perm, permType string) map[string]interface{} {
body := map[string]interface{}{
"member_id": memberID,
"member_type": memberType,
"perm": perm,
}
if bodyType := driveMemberAddBodyType(memberType, wikiSpaceMemberKind); bodyType != "" {
body["type"] = bodyType
}
if permType != "" {
body["perm_type"] = permType
}
return body
}
// readDriveMemberAddSpec parses runtime flags into a normalized request model,
// applying inference, defaults, and cross-field validation in one place.
func readDriveMemberAddSpec(runtime *common.RuntimeContext) (driveMemberAddSpec, error) {
token, resourceType, err := resolveDriveMemberAddTarget(runtime.Str("token"), runtime.Str("type"))
if err != nil {
return driveMemberAddSpec{}, err
}
// Parse member-id: comma-separated for batch.
rawMemberID := strings.TrimSpace(runtime.Str("member-id"))
if rawMemberID == "" {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-id is required and cannot be blank").WithParam("--member-id")
}
memberIDs := splitAndTrimMembers(rawMemberID)
if len(memberIDs) == 0 {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-id is required and must contain at least one non-blank ID").WithParam("--member-id")
}
if len(memberIDs) > driveMemberAddBatchLimit {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-id accepts at most %d IDs, got %d", driveMemberAddBatchLimit, len(memberIDs)).WithParam("--member-id")
}
if duplicate, first, second, ok := firstDuplicateDriveMemberID(memberIDs); ok {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument,
"--member-id contains duplicate collaborator ID %q at positions %d and %d; remove duplicates before retrying",
duplicate, first+1, second+1,
).WithParam("--member-id")
}
memberType, err := resolveDriveMemberAddMemberType(memberIDs, runtime.Str("member-type"))
if err != nil {
return driveMemberAddSpec{}, err
}
memberKind, err := resolveDriveMemberAddMemberKind(memberType, runtime.Str("member-kind"))
if err != nil {
return driveMemberAddSpec{}, err
}
// perm: default to view.
perm, err := normalizeDriveMemberAddEnumValue(runtime.Str("perm"), driveMemberAddPerms, "--perm")
if err != nil {
return driveMemberAddSpec{}, err
}
if perm == "" {
perm = "view"
}
// perm-type: only meaningful for wiki; default container except for wiki-space collaborators.
permType, err := normalizeDriveMemberAddEnumValue(runtime.Str("perm-type"), driveMemberAddPermTypes, "--perm-type")
if err != nil {
return driveMemberAddSpec{}, err
}
if resourceType == "wiki" && memberType == "wikispaceid" {
if runtime.Changed("perm-type") {
return driveMemberAddSpec{}, errs.NewValidationError(
errs.SubtypeInvalidArgument,
"--perm-type is not supported when --member-type=wikispaceid; use --member-kind wiki_space_member|wiki_space_viewer|wiki_space_editor to set the wiki-space role",
).WithParam("--perm-type")
}
permType = ""
} else if resourceType == "wiki" && permType == "" {
permType = driveMemberAddDefaultPermType(resourceType)
} else if resourceType != "wiki" && runtime.Changed("perm-type") {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--perm-type only applies when resource type is wiki; got %q", resourceType).WithParam("--perm-type")
} else if resourceType != "wiki" {
permType = ""
}
spec := driveMemberAddSpec{
Token: token,
ResourceType: resourceType,
MemberIDs: memberIDs,
MemberType: memberType,
MemberKind: memberKind,
Perm: perm,
PermType: permType,
NeedNotification: runtime.Bool("need-notification"),
NotificationSet: runtime.Changed("need-notification"),
}
if runtime.As().IsBot() && spec.NotificationSet {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--need-notification is only valid with --as user; omit it when using --as bot").WithParam("--need-notification")
}
if runtime.As().IsBot() && spec.MemberType == "opendepartmentid" {
return driveMemberAddSpec{}, errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-type=opendepartmentid requires --as user; bot identity does not support adding department collaborators").WithParam("--member-type")
}
return spec, nil
}
// resolveDriveMemberAddTarget extracts (token, type) from a user-supplied
// --token value that may be either a bare token or a full resource URL, plus an
// optional explicit --type. Explicit --type wins over URL inference.
func resolveDriveMemberAddTarget(raw, explicitType string) (token, resourceType string, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--token is required").WithParam("--token")
}
explicitType = strings.ToLower(strings.TrimSpace(explicitType))
if strings.Contains(raw, "://") {
parsed, parseErr := url.Parse(raw)
if parseErr != nil || parsed.Hostname() == "" {
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--token URL is malformed: %q", raw).WithParam("--token")
}
urlToken, urlType, ok := parseDriveMemberAddResourceURLPath(parsed.Path)
if !ok {
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
"unsupported URL path %q: expected one of %s followed by a token",
parsed.Path, strings.Join(driveMemberAddSupportedURLPaths(), ", "),
).WithParam("--token")
}
token = urlToken
if explicitType == "" {
resourceType = urlType
}
} else {
token = raw
}
if explicitType != "" {
if !isSupportedDriveMemberAddResourceType(explicitType) {
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--type must be one of: %s", strings.Join(driveMemberAddResourceTypes, ", ")).WithParam("--type")
}
resourceType = explicitType
}
if resourceType == "" {
return "", "", errs.NewValidationError(errs.SubtypeInvalidArgument,
"--type is required when --token is a bare token; accepted values: %s",
strings.Join(driveMemberAddResourceTypes, ", "),
).WithParam("--type")
}
return token, resourceType, nil
}
func driveMemberAddSupportedURLPaths() []string {
paths := make([]string, 0, len(driveMemberAddURLPathToType))
for _, mapping := range driveMemberAddURLPathToType {
paths = append(paths, mapping.Prefix)
}
return paths
}
func parseDriveMemberAddResourceURLPath(path string) (token, resourceType string, ok bool) {
for _, mapping := range driveMemberAddURLPathToType {
if !strings.HasPrefix(path, mapping.Prefix) {
continue
}
token := path[len(mapping.Prefix):]
token = strings.TrimRight(token, "/")
if idx := strings.IndexByte(token, '/'); idx >= 0 {
token = token[:idx]
}
token = strings.TrimSpace(token)
if token == "" {
return "", "", false
}
return token, mapping.Type, true
}
return "", "", false
}
func isSupportedDriveMemberAddResourceType(resourceType string) bool {
switch resourceType {
case "docx", "doc", "sheet", "bitable", "file", "folder", "wiki", "mindnote", "slides", "minutes":
return true
default:
return false
}
}
func resolveDriveMemberAddMemberType(memberIDs []string, explicit string) (string, error) {
var err error
explicit, err = normalizeDriveMemberAddEnumValue(explicit, driveMemberAddIDTypes, "--member-type")
if err != nil {
return "", err
}
if explicit == "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-type is required; accepted values: %s", strings.Join(driveMemberAddIDTypes, ", ")).WithParam("--member-type")
}
for i, memberID := range memberIDs {
if expected := inferMemberTypeFromID(memberID); expected != "" && expected != explicit {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument,
"member-id[%d] %q prefix implies --member-type %s, but --member-type %s was provided; fix the ID or use the matching member type",
i+1, memberID, expected, explicit,
).WithParam("--member-id")
}
}
return normalizeDriveMemberAddMemberType(explicit), nil
}
func resolveDriveMemberAddMemberKind(memberType, raw string) (string, error) {
memberKind, err := normalizeDriveMemberAddEnumValue(raw, driveMemberAddWikiSpaceMemberKinds, "--member-kind")
if err != nil {
return "", err
}
if memberType == "wikispaceid" {
if memberKind == "" {
return "", errs.NewValidationError(
errs.SubtypeInvalidArgument,
"--member-kind is required when --member-type=wikispaceid; allowed: %s",
strings.Join(driveMemberAddWikiSpaceMemberKinds, ", "),
).WithParam("--member-kind")
}
return memberKind, nil
}
if memberKind != "" {
return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--member-kind only applies when --member-type=wikispaceid").WithParam("--member-kind")
}
return "", nil
}
func normalizeDriveMemberAddMemberType(memberType string) string {
return strings.ToLower(strings.TrimSpace(memberType))
}
func normalizeDriveMemberAddEnumValue(raw string, allowed []string, flagName string) (string, error) {
value := strings.TrimSpace(raw)
if value == "" {
return "", nil
}
for _, candidate := range allowed {
if strings.EqualFold(value, candidate) {
return candidate, nil
}
}
return "", errs.NewValidationError(
errs.SubtypeInvalidArgument,
"invalid value %q for %s, allowed: %s",
value,
flagName,
strings.Join(allowed, ", "),
).WithParam(flagName)
}
// splitAndTrimMembers splits a comma-separated member-id string and trims whitespace.
func splitAndTrimMembers(raw string) []string {
parts := strings.Split(raw, ",")
var result []string
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}
func firstDuplicateDriveMemberID(memberIDs []string) (duplicate string, first, second int, ok bool) {
seen := make(map[string]int, len(memberIDs))
for i, memberID := range memberIDs {
if prev, exists := seen[memberID]; exists {
return memberID, prev, i, true
}
seen[memberID] = i
}
return "", 0, 0, false
}
// inferMemberTypeFromID returns the expected member_type for a member-id
// based on its prefix, or "" if no prefix matches (e.g. groupid).
func inferMemberTypeFromID(memberID string) string {
memberID = strings.TrimSpace(memberID)
if memberID == "" {
return ""
}
if strings.Contains(memberID, "@") {
return "email"
}
for prefix, mtype := range driveMemberAddPrefixToType {
if strings.HasPrefix(memberID, prefix) {
return mtype
}
}
return ""
}
// driveMemberAddDefaultPermType returns the default perm_type for a given
// resource type. For wiki nodes, container is the default for regular
// collaborators. Wiki-space collaborators omit perm_type because their role is
// carried by the body type field.
func driveMemberAddDefaultPermType(resourceType string) string {
switch resourceType {
case "wiki":
return "container"
default:
return ""
}
}
// inferDriveMemberKind derives the request-body collaborator kind from
// member-type for all supported member-type values.
func inferDriveMemberKind(memberType string) string {
switch memberType {
case "email", "openid", "unionid", "userid":
return "user"
case "openchat":
return "chat"
case "opendepartmentid":
return "department"
case "groupid":
return "group"
default:
return ""
}
}
func driveMemberAddBodyType(memberType, wikiSpaceMemberKind string) string {
if memberType == "wikispaceid" {
return wikiSpaceMemberKind
}
return inferDriveMemberKind(memberType)
}
// buildDriveMemberAddDryRun renders the exact request preview for --dry-run.
func buildDriveMemberAddDryRun(spec driveMemberAddSpec) *common.DryRunAPI {
if len(spec.MemberIDs) == 1 {
body := buildMemberBody(spec.MemberIDs[0], spec.MemberType, spec.MemberKind, spec.Perm, spec.PermType)
return common.NewDryRunAPI().
Desc("Add Drive collaborator/member permission").
POST(fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members", validate.EncodePathSegment(spec.Token))).
Params(spec.DryRunParams()).
Body(body)
}
members := buildDriveMemberAddMemberBodies(spec)
return common.NewDryRunAPI().
Desc("Batch add Drive collaborator/member permissions").
POST(fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members/batch_create", validate.EncodePathSegment(spec.Token))).
Params(spec.DryRunParams()).
Body(map[string]interface{}{"members": members})
}
// executeDriveMemberAddSingle calls the single-member create API.
func executeDriveMemberAddSingle(runtime *common.RuntimeContext, spec driveMemberAddSpec) error {
fmt.Fprintf(runtime.IO().ErrOut, "Adding Drive member %s (type=%s, perm=%s) to %s %s...\n",
common.MaskToken(spec.MemberIDs[0]), spec.MemberType, spec.Perm, spec.ResourceType, common.MaskToken(spec.Token))
body := buildMemberBody(spec.MemberIDs[0], spec.MemberType, spec.MemberKind, spec.Perm, spec.PermType)
data, err := runtime.CallAPITyped(
"POST",
fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members", validate.EncodePathSegment(spec.Token)),
spec.APIQueryParams(),
body,
)
if err != nil {
return err
}
out := driveMemberAddOutput(spec, spec.MemberIDs[0], common.GetMap(data, "member"))
fmt.Fprintf(runtime.IO().ErrOut, "Added Drive member %s\n", common.MaskToken(common.GetString(out, "member_id")))
runtime.Out(out, nil)
return nil
}
// executeDriveMemberAddBatch calls the batch_create API. A successful HTTP/API
// response is treated as complete only when the server returns every requested
// member_id, regardless of response array order.
func executeDriveMemberAddBatch(runtime *common.RuntimeContext, spec driveMemberAddSpec) error {
members := buildDriveMemberAddMemberBodies(spec)
fmt.Fprintf(runtime.IO().ErrOut, "Adding %d Drive members (type=%s, perm=%s) to %s %s...\n",
len(spec.MemberIDs), spec.MemberType, spec.Perm, spec.ResourceType, common.MaskToken(spec.Token))
data, err := runtime.CallAPITyped(
"POST",
fmt.Sprintf("/open-apis/drive/v1/permissions/%s/members/batch_create", validate.EncodePathSegment(spec.Token)),
spec.APIQueryParams(),
map[string]interface{}{"members": members},
)
if err != nil {
return wrapDriveMemberAddBatchAPIError(err)
}
result := buildDriveMemberAddBatchResult(spec, data)
if common.GetBool(result, "partial") {
return runtime.OutPartialFailure(result, nil)
}
fmt.Fprintf(runtime.IO().ErrOut, "Added %d Drive member(s)\n", result["succeeded_count"])
runtime.Out(result, nil)
return nil
}
const (
driveMemberAddInvalidParameterCode = 1063001
driveMemberAddInvalidOperationCode = 1063003
)
func wrapDriveMemberAddBatchAPIError(err error) error {
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
return err
}
wrapped := *apiErr
switch apiErr.Code {
case driveMemberAddInvalidOperationCode:
wrapped.Message = "Drive batch member add failed: one or more requested members may already be collaborators on this resource"
wrapped.Hint = "For batch add, remove members that already have access (especially a bot/app being added again), then retry only the missing collaborators."
case driveMemberAddInvalidParameterCode:
wrapped.Message = "Drive batch member add failed: one or more requested members may be invalid for this resource or identity"
wrapped.Hint = "Check whether each --member-id exists, belongs to the same tenant, and is visible to the current identity; remove invalid members and retry only the valid collaborators."
default:
return err
}
wrapped.Cause = err
return &wrapped
}
func buildDriveMemberAddMemberBodies(spec driveMemberAddSpec) []map[string]interface{} {
members := make([]map[string]interface{}, len(spec.MemberIDs))
for i, mid := range spec.MemberIDs {
members[i] = buildMemberBody(mid, spec.MemberType, spec.MemberKind, spec.Perm, spec.PermType)
}
return members
}
func buildDriveMemberAddBatchResult(spec driveMemberAddSpec, data map[string]interface{}) map[string]interface{} {
rawMembers, _ := data["members"].([]interface{})
// Build set of requested IDs for O(1) lookup.
requestedSet := make(map[string]bool, len(spec.MemberIDs))
for _, id := range spec.MemberIDs {
requestedSet[id] = true
}
// First pass: build returned map and results array.
// Matching is done by member_id, not by array index, so the server may
// return members in any order without causing false partial_failure.
results := make([]map[string]interface{}, 0, len(rawMembers))
succeededIDs := make(map[string]bool, len(rawMembers))
var mismatched []map[string]interface{}
for _, raw := range rawMembers {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
rawMemberID := common.GetString(m, "member_id")
out := driveMemberAddOutputWithOptions(spec, "", m, false)
results = append(results, out)
if rawMemberID != "" {
if requestedSet[rawMemberID] {
succeededIDs[rawMemberID] = true
} else {
mismatched = append(mismatched, map[string]interface{}{
"returned": rawMemberID,
})
}
}
}
// Second pass: find requested IDs missing from the response.
missing := make([]string, 0)
for _, memberID := range spec.MemberIDs {
if !succeededIDs[memberID] {
missing = append(missing, memberID)
}
}
partial := len(results) != len(spec.MemberIDs) || len(missing) > 0 || len(mismatched) > 0
result := map[string]interface{}{
"resource_token": spec.Token,
"resource_type": spec.ResourceType,
"requested_count": len(spec.MemberIDs),
"succeeded_count": len(succeededIDs),
"partial": partial,
"members": results,
"missing_member_ids": missing,
}
if len(mismatched) > 0 {
result["mismatched_member_ids"] = mismatched
}
return result
}
// driveMemberAddOutput flattens the server response into a stable envelope and
// backfills fields from spec when the server omits them.
func driveMemberAddOutput(spec driveMemberAddSpec, fallbackMemberID string, raw map[string]interface{}) map[string]interface{} {
return driveMemberAddOutputWithOptions(spec, fallbackMemberID, raw, true)
}
func driveMemberAddOutputWithOptions(spec driveMemberAddSpec, fallbackMemberID string, raw map[string]interface{}, allowDefaultMemberID bool) map[string]interface{} {
out := map[string]interface{}{
"resource_token": spec.Token,
"resource_type": spec.ResourceType,
}
if raw != nil {
for _, key := range []string{"member_id", "member_type", "perm", "type"} {
if v, ok := raw[key]; ok {
out[key] = v
}
}
if spec.ResourceType == "wiki" {
if v, ok := raw["perm_type"]; ok {
out["perm_type"] = v
}
}
}
if common.GetString(out, "member_id") == "" {
if fallbackMemberID == "" && allowDefaultMemberID && len(spec.MemberIDs) > 0 {
fallbackMemberID = spec.MemberIDs[0]
}
if fallbackMemberID != "" {
out["member_id"] = fallbackMemberID
}
}
if common.GetString(out, "member_type") == "" {
out["member_type"] = spec.MemberType
}
if common.GetString(out, "perm") == "" {
out["perm"] = spec.Perm
}
if spec.PermType != "" && common.GetString(out, "perm_type") == "" {
out["perm_type"] = spec.PermType
}
if bodyType := driveMemberAddBodyType(spec.MemberType, spec.MemberKind); bodyType != "" && common.GetString(out, "type") == "" {
out["type"] = bodyType
}
if t := common.GetString(out, "type"); t != "" {
out["member_kind"] = t
}
delete(out, "type")
return out
}