diff --git a/gitea/Issue_label_test.go b/gitea/Issue_label_test.go new file mode 100644 index 0000000..c53aa5a --- /dev/null +++ b/gitea/Issue_label_test.go @@ -0,0 +1,40 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package gitea + +import ( + "log" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestLabels test label related func +func TestLabels(t *testing.T) { + log.Println("== TestLabels ==") + c := newTestClient() + repo, err := createTestRepo(t, "LabelTestsRepo", c) + assert.NoError(t, err) + + createOpts := CreateLabelOption{ + Name: " ", + Description: "", + Color: "", + } + err = createOpts.Validate() + assert.Error(t, err) + assert.EqualValues(t, "invalid color format", err.Error()) + createOpts.Color = "12345f" + err = createOpts.Validate() + assert.Error(t, err) + assert.EqualValues(t, "empty name not allowed", err.Error()) + createOpts.Name = "label one" + + label1, err := c.CreateLabel(repo.Owner.UserName, repo.Name, createOpts) + assert.NoError(t, err) + assert.EqualValues(t, createOpts.Name, label1.Name) + assert.EqualValues(t, createOpts.Color, label1.Color) + +} diff --git a/gitea/admin_user.go b/gitea/admin_user.go index c447934..13a8776 100644 --- a/gitea/admin_user.go +++ b/gitea/admin_user.go @@ -35,8 +35,22 @@ type CreateUserOption struct { SendNotify bool `json:"send_notify"` } +// Validate the CreateUserOption struct +func (opt CreateUserOption) Validate() error { + if len(opt.Email) == 0 { + return fmt.Errorf("email is empty") + } + if len(opt.Username) == 0 { + return fmt.Errorf("username is empty") + } + return nil +} + // AdminCreateUser create a user func (c *Client) AdminCreateUser(opt CreateUserOption) (*User, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/hook.go b/gitea/hook.go index f89a6e9..1ca1a3b 100644 --- a/gitea/hook.go +++ b/gitea/hook.go @@ -64,8 +64,19 @@ type CreateHookOption struct { Active bool `json:"active"` } +// Validate the CreateHookOption struct +func (opt CreateHookOption) Validate() error { + if len(opt.Type) == 0 { + return fmt.Errorf("hook type needed") + } + return nil +} + // CreateOrgHook create one hook for an organization, with options func (c *Client) CreateOrgHook(org string, opt CreateHookOption) (*Hook, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/issue.go b/gitea/issue.go index 62c0313..dd591f3 100644 --- a/gitea/issue.go +++ b/gitea/issue.go @@ -174,8 +174,19 @@ type CreateIssueOption struct { Closed bool `json:"closed"` } +// Validate the CreateIssueOption struct +func (opt CreateIssueOption) Validate() error { + if len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + return nil +} + // CreateIssue create a new issue for a given repository func (c *Client) CreateIssue(owner, repo string, opt CreateIssueOption) (*Issue, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -196,8 +207,19 @@ type EditIssueOption struct { Deadline *time.Time `json:"due_date"` } +// Validate the EditIssueOption struct +func (opt EditIssueOption) Validate() error { + if len(opt.Title) != 0 && len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + return nil +} + // EditIssue modify an existing issue for a given repository func (c *Client) EditIssue(owner, repo string, index int64, opt EditIssueOption) (*Issue, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/issue_comment.go b/gitea/issue_comment.go index cc8cf9f..1c0a1de 100644 --- a/gitea/issue_comment.go +++ b/gitea/issue_comment.go @@ -77,8 +77,19 @@ type CreateIssueCommentOption struct { Body string `json:"body"` } +// Validate the CreateIssueCommentOption struct +func (opt CreateIssueCommentOption) Validate() error { + if len(opt.Body) == 0 { + return fmt.Errorf("body is empty") + } + return nil +} + // CreateIssueComment create comment on an issue. func (c *Client) CreateIssueComment(owner, repo string, index int64, opt CreateIssueCommentOption) (*Comment, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -92,8 +103,19 @@ type EditIssueCommentOption struct { Body string `json:"body"` } +// Validate the EditIssueCommentOption struct +func (opt EditIssueCommentOption) Validate() error { + if len(opt.Body) == 0 { + return fmt.Errorf("body is empty") + } + return nil +} + // EditIssueComment edits an issue comment. func (c *Client) EditIssueComment(owner, repo string, commentID int64, opt EditIssueCommentOption) (*Comment, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/issue_label.go b/gitea/issue_label.go index c5e560a..d067d70 100644 --- a/gitea/issue_label.go +++ b/gitea/issue_label.go @@ -8,6 +8,8 @@ import ( "bytes" "encoding/json" "fmt" + "regexp" + "strings" ) // Label a label to an issue or a pr @@ -33,7 +35,6 @@ func (c *Client) ListRepoLabels(owner, repo string, opt ListLabelsOptions) ([]*L } // GetRepoLabel get one label of repository by repo it -// TODO: maybe we need get a label by name func (c *Client) GetRepoLabel(owner, repo string, id int64) (*Label, error) { label := new(Label) return label, c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil, label) @@ -47,8 +48,26 @@ type CreateLabelOption struct { Description string `json:"description"` } +// Validate the CreateLabelOption struct +func (opt CreateLabelOption) Validate() error { + aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", opt.Color) + if err != nil { + return err + } + if !aw { + return fmt.Errorf("invalid color format") + } + if len(strings.TrimSpace(opt.Name)) == 0 { + return fmt.Errorf("empty name not allowed") + } + return nil +} + // CreateLabel create one label of repository func (c *Client) CreateLabel(owner, repo string, opt CreateLabelOption) (*Label, error) { + if err := opt.Validate(); err != nil { + return nil, err + } if len(opt.Color) == 6 { if err := c.CheckServerVersionConstraint(">=1.12.0"); err != nil { opt.Color = "#" + opt.Color @@ -70,8 +89,30 @@ type EditLabelOption struct { Description *string `json:"description"` } +// Validate the EditLabelOption struct +func (opt EditLabelOption) Validate() error { + if opt.Color != nil { + aw, err := regexp.MatchString("^#?[0-9,a-f,A-F]{6}$", *opt.Color) + if err != nil { + return err + } + if !aw { + return fmt.Errorf("invalid color format") + } + } + if opt.Name != nil { + if len(strings.TrimSpace(*opt.Name)) == 0 { + return fmt.Errorf("empty name not allowed") + } + } + return nil +} + // EditLabel modify one label with options func (c *Client) EditLabel(owner, repo string, id int64, opt EditLabelOption) (*Label, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -81,7 +122,6 @@ func (c *Client) EditLabel(owner, repo string, id int64, opt EditLabelOption) (* } // DeleteLabel delete one label of repository by id -// TODO: maybe we need delete by name func (c *Client) DeleteLabel(owner, repo string, id int64) error { _, err := c.getResponse("DELETE", fmt.Sprintf("/repos/%s/%s/labels/%d", owner, repo, id), nil, nil) return err diff --git a/gitea/issue_milestone.go b/gitea/issue_milestone.go index 5770ffe..1c24873 100644 --- a/gitea/issue_milestone.go +++ b/gitea/issue_milestone.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "time" ) @@ -63,8 +64,19 @@ type CreateMilestoneOption struct { Deadline *time.Time `json:"due_on"` } +// Validate the CreateMilestoneOption struct +func (opt CreateMilestoneOption) Validate() error { + if len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + return nil +} + // CreateMilestone create one milestone with options func (c *Client) CreateMilestone(owner, repo string, opt CreateMilestoneOption) (*Milestone, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -81,8 +93,19 @@ type EditMilestoneOption struct { Deadline *time.Time `json:"due_on"` } +// Validate the EditMilestoneOption struct +func (opt EditMilestoneOption) Validate() error { + if len(opt.Title) != 0 && len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + return nil +} + // EditMilestone modify milestone with options func (c *Client) EditMilestone(owner, repo string, id int64, opt EditMilestoneOption) (*Milestone, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/issue_tracked_time.go b/gitea/issue_tracked_time.go index 51f1a99..d4b6af6 100644 --- a/gitea/issue_tracked_time.go +++ b/gitea/issue_tracked_time.go @@ -46,15 +46,26 @@ func (c *Client) GetMyTrackedTimes() ([]*TrackedTime, error) { // AddTimeOption options for adding time to an issue type AddTimeOption struct { // time in seconds - Time int64 `json:"time" binding:"Required"` + Time int64 `json:"time"` // optional Created time.Time `json:"created"` // optional User string `json:"user_name"` } +// Validate the AddTimeOption struct +func (opt AddTimeOption) Validate() error { + if opt.Time == 0 { + return fmt.Errorf("no time to add") + } + return nil +} + // AddTime adds time to issue with the given index func (c *Client) AddTime(owner, repo string, index int64, opt AddTimeOption) (*TrackedTime, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/org.go b/gitea/org.go index 61ade30..0e98adb 100644 --- a/gitea/org.go +++ b/gitea/org.go @@ -56,12 +56,30 @@ type CreateOrgOption struct { Website string `json:"website"` Location string `json:"location"` // possible values are `public` (default), `limited` or `private` - // enum: public,limited,private Visibility string `json:"visibility"` } +// checkVisibilityOpt check if mode exist +func checkVisibilityOpt(v string) bool { + return v == "public" || v == "limited" || v == "private" +} + +// Validate the CreateOrgOption struct +func (opt CreateOrgOption) Validate() error { + if len(opt.UserName) == 0 { + return fmt.Errorf("empty org name") + } + if len(opt.Visibility) != 0 && !checkVisibilityOpt(opt.Visibility) { + return fmt.Errorf("infalid bisibility option") + } + return nil +} + // CreateOrg creates an organization func (c *Client) CreateOrg(opt CreateOrgOption) (*Organization, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -77,12 +95,22 @@ type EditOrgOption struct { Website string `json:"website"` Location string `json:"location"` // possible values are `public`, `limited` or `private` - // enum: public,limited,private Visibility string `json:"visibility"` } +// Validate the EditOrgOption struct +func (opt EditOrgOption) Validate() error { + if len(opt.Visibility) != 0 && !checkVisibilityOpt(opt.Visibility) { + return fmt.Errorf("infalid bisibility option") + } + return nil +} + // EditOrg modify one organization via options func (c *Client) EditOrg(orgname string, opt EditOrgOption) error { + if err := opt.Validate(); err != nil { + return err + } body, err := json.Marshal(&opt) if err != nil { return err @@ -93,6 +121,6 @@ func (c *Client) EditOrg(orgname string, opt EditOrgOption) error { // DeleteOrg deletes an organization func (c *Client) DeleteOrg(orgname string) error { - _, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s", orgname), nil, nil) + _, err := c.getResponse("DELETE", fmt.Sprintf("/orgs/%s", orgname), jsonHeader, nil) return err } diff --git a/gitea/pull.go b/gitea/pull.go index a5de7ed..d1659dc 100644 --- a/gitea/pull.go +++ b/gitea/pull.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "time" ) @@ -149,8 +150,24 @@ type EditPullRequestOption struct { Deadline *time.Time `json:"due_date"` } +// Validate the EditPullRequestOption struct +func (opt EditPullRequestOption) Validate(c *Client) error { + if len(opt.Title) != 0 && len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + if len(opt.Base) != 0 { + if err := c.CheckServerVersionConstraint(">=1.12.0"); err != nil { + return fmt.Errorf("can not change base gitea to old") + } + } + return nil +} + // EditPullRequest modify pull request with PR id and options func (c *Client) EditPullRequest(owner, repo string, index int64, opt EditPullRequestOption) (*PullRequest, error) { + if err := opt.Validate(c); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -167,13 +184,21 @@ type MergePullRequestOption struct { Message string `json:"MergeMessageField"` } -// MergePullRequest merge a PR to repository by PR id -func (c *Client) MergePullRequest(owner, repo string, index int64, opt MergePullRequestOption) (bool, error) { +// Validate the MergePullRequestOption struct +func (opt MergePullRequestOption) Validate(c *Client) error { if opt.Style == MergeStyleSquash { if err := c.CheckServerVersionConstraint(">=1.11.5"); err != nil { - return false, err + return err } } + return nil +} + +// MergePullRequest merge a PR to repository by PR id +func (c *Client) MergePullRequest(owner, repo string, index int64, opt MergePullRequestOption) (bool, error) { + if err := opt.Validate(c); err != nil { + return false, err + } body, err := json.Marshal(&opt) if err != nil { return false, err diff --git a/gitea/pull_review.go b/gitea/pull_review.go index 9ff708a..8e5a5e8 100644 --- a/gitea/pull_review.go +++ b/gitea/pull_review.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "time" ) @@ -40,8 +41,7 @@ type PullReview struct { Stale bool `json:"stale"` Official bool `json:"official"` CodeCommentsCount int `json:"comments_count"` - // swagger:strfmt date-time - Submitted time.Time `json:"submitted_at"` + Submitted time.Time `json:"submitted_at"` HTMLURL string `json:"html_url"` HTMLPullURL string `json:"pull_request_url"` @@ -54,9 +54,7 @@ type PullReviewComment struct { Reviewer *User `json:"user"` ReviewID int64 `json:"pull_request_review_id"` - // swagger:strfmt date-time Created time.Time `json:"created_at"` - // swagger:strfmt date-time Updated time.Time `json:"updated_at"` Path string `json:"path"` @@ -100,6 +98,38 @@ type ListPullReviewsOptions struct { ListOptions } +// Validate the CreatePullReviewOptions struct +func (opt CreatePullReviewOptions) Validate() error { + if opt.State != ReviewStateApproved && len(strings.TrimSpace(opt.Body)) == 0 { + return fmt.Errorf("body is empty") + } + for i := range opt.Comments { + if err := opt.Comments[i].Validate(); err != nil { + return err + } + } + return nil +} + +// Validate the SubmitPullReviewOptions struct +func (opt SubmitPullReviewOptions) Validate() error { + if opt.State != ReviewStateApproved && len(strings.TrimSpace(opt.Body)) == 0 { + return fmt.Errorf("body is empty") + } + return nil +} + +// Validate the CreatePullReviewComment struct +func (opt CreatePullReviewComment) Validate() error { + if len(strings.TrimSpace(opt.Body)) == 0 { + return fmt.Errorf("body is empty") + } + if opt.NewLineNum != 0 && opt.OldLineNum != 0 { + return fmt.Errorf("old and new line num are set, cant identify the code comment position") + } + return nil +} + // ListPullReviews lists all reviews of a pull request func (c *Client) ListPullReviews(owner, repo string, index int64, opt ListPullReviewsOptions) ([]*PullReview, error) { if err := c.CheckServerVersionConstraint(">=1.12.0"); err != nil { @@ -158,6 +188,9 @@ func (c *Client) CreatePullReview(owner, repo string, index int64, opt CreatePul if err := c.CheckServerVersionConstraint(">=1.12.0"); err != nil { return nil, err } + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -173,6 +206,9 @@ func (c *Client) SubmitPullReview(owner, repo string, index, id int64, opt Submi if err := c.CheckServerVersionConstraint(">=1.12.0"); err != nil { return nil, err } + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/release.go b/gitea/release.go index fe5c023..44cdc00 100644 --- a/gitea/release.go +++ b/gitea/release.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/json" "fmt" + "strings" "time" ) @@ -63,9 +64,20 @@ type CreateReleaseOption struct { IsPrerelease bool `json:"prerelease"` } +// Validate the CreateReleaseOption struct +func (opt CreateReleaseOption) Validate() error { + if len(strings.TrimSpace(opt.Title)) == 0 { + return fmt.Errorf("title is empty") + } + return nil +} + // CreateRelease create a release -func (c *Client) CreateRelease(user, repo string, form CreateReleaseOption) (*Release, error) { - body, err := json.Marshal(form) +func (c *Client) CreateRelease(user, repo string, opt CreateReleaseOption) (*Release, error) { + if err := opt.Validate(); err != nil { + return nil, err + } + body, err := json.Marshal(opt) if err != nil { return nil, err } diff --git a/gitea/repo.go b/gitea/repo.go index 80f24b8..bd47650 100644 --- a/gitea/repo.go +++ b/gitea/repo.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "net/url" + "strings" "time" ) @@ -161,7 +162,6 @@ func (c *Client) SearchRepos(opt SearchRepoOptions) ([]*Repository, error) { // CreateRepoOption options when creating repository type CreateRepoOption struct { // Name of the repository to create - // Name string `json:"name"` // Description of the repository to create Description string `json:"description"` @@ -181,8 +181,19 @@ type CreateRepoOption struct { DefaultBranch string `json:"default_branch"` } +// Validate the CreateRepoOption struct +func (opt CreateRepoOption) Validate() error { + if len(strings.TrimSpace(opt.Name)) == 0 { + return fmt.Errorf("name is empty") + } + return nil +} + // CreateRepo creates a repository for authenticated user. func (c *Client) CreateRepo(opt CreateRepoOption) (*Repository, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err @@ -193,6 +204,9 @@ func (c *Client) CreateRepo(opt CreateRepoOption) (*Repository, error) { // CreateOrgRepo creates an organization repository for authenticated user. func (c *Client) CreateOrgRepo(org string, opt CreateRepoOption) (*Repository, error) { + if err := opt.Validate(); err != nil { + return nil, err + } body, err := json.Marshal(&opt) if err != nil { return nil, err diff --git a/gitea/repo_branch_protection.go b/gitea/repo_branch_protection.go index b7ef96c..31bba9c 100644 --- a/gitea/repo_branch_protection.go +++ b/gitea/repo_branch_protection.go @@ -14,30 +14,28 @@ import ( // BranchProtection represents a branch protection for a repository type BranchProtection struct { - BranchName string `json:"branch_name"` - EnablePush bool `json:"enable_push"` - EnablePushWhitelist bool `json:"enable_push_whitelist"` - PushWhitelistUsernames []string `json:"push_whitelist_usernames"` - PushWhitelistTeams []string `json:"push_whitelist_teams"` - PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` - EnableMergeWhitelist bool `json:"enable_merge_whitelist"` - MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` - MergeWhitelistTeams []string `json:"merge_whitelist_teams"` - EnableStatusCheck bool `json:"enable_status_check"` - StatusCheckContexts []string `json:"status_check_contexts"` - RequiredApprovals int64 `json:"required_approvals"` - EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"` - ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` - ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` - BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` - BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` - DismissStaleApprovals bool `json:"dismiss_stale_approvals"` - RequireSignedCommits bool `json:"require_signed_commits"` - ProtectedFilePatterns string `json:"protected_file_patterns"` - // swagger:strfmt date-time - Created time.Time `json:"created_at"` - // swagger:strfmt date-time - Updated time.Time `json:"updated_at"` + BranchName string `json:"branch_name"` + EnablePush bool `json:"enable_push"` + EnablePushWhitelist bool `json:"enable_push_whitelist"` + PushWhitelistUsernames []string `json:"push_whitelist_usernames"` + PushWhitelistTeams []string `json:"push_whitelist_teams"` + PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys"` + EnableMergeWhitelist bool `json:"enable_merge_whitelist"` + MergeWhitelistUsernames []string `json:"merge_whitelist_usernames"` + MergeWhitelistTeams []string `json:"merge_whitelist_teams"` + EnableStatusCheck bool `json:"enable_status_check"` + StatusCheckContexts []string `json:"status_check_contexts"` + RequiredApprovals int64 `json:"required_approvals"` + EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist"` + ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"` + ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams"` + BlockOnRejectedReviews bool `json:"block_on_rejected_reviews"` + BlockOnOutdatedBranch bool `json:"block_on_outdated_branch"` + DismissStaleApprovals bool `json:"dismiss_stale_approvals"` + RequireSignedCommits bool `json:"require_signed_commits"` + ProtectedFilePatterns string `json:"protected_file_patterns"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"updated_at"` } // CreateBranchProtectionOption options for creating a branch protection diff --git a/gitea/repo_collaborator.go b/gitea/repo_collaborator.go index e13ccdb..dbb72b2 100644 --- a/gitea/repo_collaborator.go +++ b/gitea/repo_collaborator.go @@ -41,8 +41,20 @@ type AddCollaboratorOption struct { Permission *string `json:"permission"` } +// Validate the AddCollaboratorOption struct +func (opt AddCollaboratorOption) Validate() error { + if opt.Permission != nil && + *opt.Permission != "read" && *opt.Permission != "write" && *opt.Permission != "admin" { + return fmt.Errorf("permission mode invalid") + } + return nil +} + // AddCollaborator add some user as a collaborator of a repository func (c *Client) AddCollaborator(user, repo, collaborator string, opt AddCollaboratorOption) error { + if err := opt.Validate(); err != nil { + return err + } body, err := json.Marshal(&opt) if err != nil { return err