diff --git a/README.md b/README.md index ec33b1e..8d6e0d6 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ trickest list --project --space |-----------|---------|---------|----------------------------------------------------| | --project | string | / | The name of the project to be listed. | | --space | string | / | The name of the space to which the project belongs | -| --json | boolean | / | Display output in JSON format | +| --json | boolean | false | Display output in JSON format | | --url | string | / | URL for referencing a space | @@ -109,8 +109,8 @@ trickest get --workflow --space [--watch] | --project | string | / | The name of the project to which the workflow belongs | | --workflow | string | / | The name of the workflow | | --run | string | / | Get the status of a specific run | -| --watch | boolean | / | Option to track execution status in case workflow is in running state | -| --json | boolean | / | Display output in JSON format | +| --watch | boolean | false | Option to track execution status in case workflow is in running state | +| --json | boolean | false | Display output in JSON format | | --url | string | / | URL for referencing a space | ##### If the supplied workflow has a running execution, you can jump in and watch it running with the `--watch` flag! @@ -126,12 +126,12 @@ trickest execute --workflow --space --confi |------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------------| | --config | file | / | YAML file for run configuration | | --workflow | string | / | Workflow from the Library to be executed | -| --max | boolean | / | Use maximum number of machines for workflow execution | +| --max | boolean | false | Use maximum number of machines for workflow execution | | --output | string | / | A comma-separated list of nodes whose outputs should be downloaded when the execution is finished | -| --output-all | boolean | / | Download all outputs when the execution is finished | +| --output-all | boolean | false | Download all outputs when the execution is finished | | --output-dir | string | . | Path to the directory which should be used to store outputs | -| --show-params | boolean | / | Show parameters in the workflow tree | -| --watch | boolean | / | Option to track execution status in case workflow is in running state | +| --show-params | boolean | false | Show parameters in the workflow tree | +| --watch | boolean | false | Option to track execution status in case workflow is in running state | | --set-name | string | / | Sets the new workflow name and will copy the workflow to space and project supplied | | --ci | boolean | false | Enable CI mode (in-progress executions will be stopped when the CLI is forcefully stopped - if not set, you will be asked for confirmation) | | --create-project | boolean | false | If the project doesn't exist, create one using the project flag as its name (or workflow/tool name if project flag is not set) | @@ -227,9 +227,6 @@ Use **library search** to search all Trickest tools & workflows available in the trickest library search subdomain takeover ``` -[](https://trickest.io/auth/register) - - ## Files command Interact with the Trickest file storage @@ -244,7 +241,7 @@ trickest files get --file my_file.txt --output-dir out |----------------------|--------|----------|---------------------------------------------------------------------| | --file | string | / | File or files (comma-separated) | | --output-dir | string | / | Path to directory which should be used to store files (default ".") | -| --partial-name-match | boolean | / | Get all files with a partial name match | +| --partial-name-match | boolean | false | Get all files with a partial name match | #### Create files Use the **create** command with the **--file** flag to upload one or more files @@ -270,7 +267,52 @@ trickest files delete --file delete_me.txt | --file | string | / | File or files (comma-separated) | +## Tools command +Manage [private tools](https://trickest.com/docs/tutorials/private-tools/private-tools-library/) + +⚒️ Learn how to add your first tool integration [here](https://trickest.com/docs/tutorials/private-tools/dockerfile-and-trickest-yaml/). + +#### Create a new private tool integration +``` +trickest tools create --file tool.yaml +``` + +| Flag | Type | Default | Description | +|----------------------|--------|----------|---------------------------------------------------------------------| +| --file | string | / | YAML file for tool definition | + +#### Update a private tool integration +``` +trickest tools update --file tool.yaml +``` + +| Flag | Type | Default | Description | +|----------------------|--------|----------|---------------------------------------------------------------------| +| --file | string | / | YAML file for tool definition | + +#### List private tool integrations +``` +trickest tools list +``` + +| Flag | Type | Default | Description | +|----------------------|---------|-------------|---------------------------------------------------------------------| +| --json | boolean | false | Display output in JSON format | + +#### Delete a private tool integration +``` +trickest tools delete --name "my-tool" +``` + +| Flag | Type | Default | Description | +|----------------------|--------|----------|---------------------------------------------------------------------| +| --id | string | / | ID of the tool to delete | +| --name | string | / | Name of the tool to delete | + ## Report Bugs / Feedback We look forward to any feedback you want to share with us or if you're stuck with a problem you can contact us at [support@trickest.com](mailto:support@trickest.com). You can also create an [Issue](https://github.com/trickest/trickest-cli/issues/new/choose) in the Github repository. + +[](https://trickest.io/auth/register) + diff --git a/cmd/library/library.go b/cmd/library/library.go index ae8102b..5214fd9 100644 --- a/cmd/library/library.go +++ b/cmd/library/library.go @@ -1,7 +1,13 @@ package library import ( + "encoding/json" + "fmt" + "strings" + "github.com/spf13/cobra" + "github.com/trickest/trickest-cli/types" + "github.com/xlab/treeprint" ) var ( @@ -27,3 +33,26 @@ func init() { command.Root().HelpFunc()(command, strings) }) } + +func PrintTools(tools []types.Tool, jsonOutput bool) { + var output string + if jsonOutput { + data, err := json.Marshal(tools) + if err != nil { + fmt.Println("Error marshalling project data") + return + } + output = string(data) + } else { + tree := treeprint.New() + tree.SetValue("Tools") + for _, tool := range tools { + branch := tree.AddBranch(tool.Name + " [" + strings.TrimPrefix(tool.SourceURL, "https://") + "]") + branch.AddNode("\U0001f4cb \033[3m" + tool.Description + "\033[0m") //📋 + } + + output = tree.String() + } + + fmt.Println(output) +} diff --git a/cmd/library/libraryListTools.go b/cmd/library/libraryListTools.go index abe47bd..0188142 100644 --- a/cmd/library/libraryListTools.go +++ b/cmd/library/libraryListTools.go @@ -1,15 +1,11 @@ package library import ( - "encoding/json" "fmt" "math" - "strings" "github.com/spf13/cobra" "github.com/trickest/trickest-cli/cmd/list" - "github.com/trickest/trickest-cli/types" - "github.com/xlab/treeprint" ) // libraryListToolsCmd represents the libraryListTools command @@ -20,7 +16,7 @@ var libraryListToolsCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { tools := list.GetTools(math.MaxInt, "", "") if len(tools) > 0 { - printTools(tools, jsonOutput) + PrintTools(tools, jsonOutput) } else { fmt.Println("Couldn't find any tool in the library!") } @@ -31,26 +27,3 @@ func init() { libraryListCmd.AddCommand(libraryListToolsCmd) libraryListToolsCmd.Flags().BoolVar(&jsonOutput, "json", false, "Display output in JSON format") } - -func printTools(tools []types.Tool, jsonOutput bool) { - var output string - if jsonOutput { - data, err := json.Marshal(tools) - if err != nil { - fmt.Println("Error marshalling project data") - return - } - output = string(data) - } else { - tree := treeprint.New() - tree.SetValue("Tools") - for _, tool := range tools { - branch := tree.AddBranch(tool.Name + " [" + strings.TrimPrefix(tool.SourceURL, "https://") + "]") - branch.AddNode("\U0001f4cb \033[3m" + tool.Description + "\033[0m") //📋 - } - - output = tree.String() - } - - fmt.Println(output) -} diff --git a/cmd/library/librarySearch.go b/cmd/library/librarySearch.go index d173dd1..5876172 100644 --- a/cmd/library/librarySearch.go +++ b/cmd/library/librarySearch.go @@ -37,7 +37,7 @@ var librarySearchCmd = &cobra.Command{ fmt.Println(output) } else { if len(tools) > 0 { - printTools(tools, jsonOutput) + PrintTools(tools, jsonOutput) } else { fmt.Println("Couldn't find any tool in the library that matches the search!") } diff --git a/cmd/root.go b/cmd/root.go index 1f6807f..d95214a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/trickest/trickest-cli/cmd/library" "github.com/trickest/trickest-cli/cmd/list" "github.com/trickest/trickest-cli/cmd/output" + "github.com/trickest/trickest-cli/cmd/tools" "github.com/trickest/trickest-cli/util" "github.com/spf13/cobra" @@ -52,6 +53,7 @@ func init() { RootCmd.AddCommand(execute.ExecuteCmd) RootCmd.AddCommand(get.GetCmd) RootCmd.AddCommand(files.FilesCmd) + RootCmd.AddCommand(tools.ToolsCmd) // RootCmd.AddCommand(export.ExportCmd) } diff --git a/cmd/tools/tools.go b/cmd/tools/tools.go new file mode 100644 index 0000000..3b640b0 --- /dev/null +++ b/cmd/tools/tools.go @@ -0,0 +1,154 @@ +package tools + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "github.com/go-yaml/yaml" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/trickest/trickest-cli/client/request" + "github.com/trickest/trickest-cli/types" + "github.com/trickest/trickest-cli/util" +) + +var toolOutputTypes = map[string]string{ + "file": "2", + "folder": "3", +} + +var ToolsCmd = &cobra.Command{ + Use: "tools", + Short: "Manage private tools", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func init() { + ToolsCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + _ = ToolsCmd.Flags().MarkHidden("workflow") + _ = ToolsCmd.Flags().MarkHidden("project") + _ = ToolsCmd.Flags().MarkHidden("space") + _ = ToolsCmd.Flags().MarkHidden("url") + + command.Root().HelpFunc()(command, strings) + }) +} + +func ListPrivateTools(name string) ([]types.Tool, error) { + endpoint := "library/tool/?public=False" + endpoint += fmt.Sprintf("&vault=%s", util.GetVault()) + if name != "" { + endpoint += "&search=" + name + } else { + endpoint += "&page_size=100" + } + + resp := request.Trickest.Get().Do(endpoint) + if resp == nil || resp.Status() != http.StatusOK { + request.ProcessUnexpectedResponse(resp) + } + + var tools types.Tools + err := json.Unmarshal(resp.Body(), &tools) + if err != nil { + return nil, fmt.Errorf("couldn't parse API response: %s", err) + } + + return tools.Results, nil +} + +func getToolIDByName(name string) (uuid.UUID, error) { + tools, err := ListPrivateTools(name) + if err != nil { + return uuid.Nil, fmt.Errorf("couldn't search for %s: %w", name, err) + } + + if len(tools) == 0 { + return uuid.Nil, fmt.Errorf("couldn't find tool '%s'", name) + } + + if len(tools) > 1 { + return uuid.Nil, fmt.Errorf("found more than one match for '%s'", name) + } + + return tools[0].ID, nil +} + +func createToolImportRequestFromYAML(fileName string) (types.ToolImportRequest, error) { + data, err := os.ReadFile(fileName) + if err != nil { + err = fmt.Errorf("couldn't read %s: %w", fileName, err) + return types.ToolImportRequest{}, err + } + + var toolImportRequest types.ToolImportRequest + err = yaml.Unmarshal(data, &toolImportRequest) + if err != nil { + err = fmt.Errorf("couldn't parse %s: %w", fileName, err) + return types.ToolImportRequest{}, err + } + + categoryID, err := util.GetCategoryIDByName(toolImportRequest.Category) + if err != nil { + err = fmt.Errorf("couldn't use the category '%s': %w", toolImportRequest.Category, err) + return types.ToolImportRequest{}, err + } + + toolImportRequest.CategoryID = categoryID + toolImportRequest.VaultInfo = util.GetVault() + toolImportRequest.OutputType = toolOutputTypes[toolImportRequest.OutputType] + for name := range toolImportRequest.Inputs { + if input, ok := toolImportRequest.Inputs[name]; ok { + input.Type = strings.ToUpper(toolImportRequest.Inputs[name].Type) + toolImportRequest.Inputs[name] = input + } + } + + return toolImportRequest, nil +} + +func importTool(fileName string, isUpdate bool) (string, uuid.UUID, error) { + toolImportRequest, err := createToolImportRequestFromYAML(fileName) + if err != nil { + return "", uuid.Nil, err + } + + toolJSON, err := json.Marshal(toolImportRequest) + if err != nil { + return "", uuid.Nil, fmt.Errorf("couldn't encode %s: %w", fileName, err) + } + + var resp *request.Response + if isUpdate { + toolName := toolImportRequest.Name + toolID, err := getToolIDByName(toolName) + if err != nil { + return "", uuid.Nil, fmt.Errorf("couldn't import '%s': %w", toolName, err) + } + resp = request.Trickest.Patch().Body(toolJSON).DoF("library/tool/%s/", toolID.String()) + } else { + resp = request.Trickest.Post().Body(toolJSON).Do("library/tool/") + } + + if resp == nil { + return "", uuid.Nil, fmt.Errorf("couldn't import %s", fileName) + } + + if resp.Status() != http.StatusCreated && resp.Status() != http.StatusOK { + request.ProcessUnexpectedResponse(resp) + } + + var importedTool types.Tool + err = json.Unmarshal(resp.Body(), &importedTool) + if err != nil { + return "", uuid.Nil, fmt.Errorf("couldn't import %s: %w", fileName, err) + } + + return importedTool.Name, importedTool.ID, nil +} diff --git a/cmd/tools/toolsCreate.go b/cmd/tools/toolsCreate.go new file mode 100644 index 0000000..97b78f1 --- /dev/null +++ b/cmd/tools/toolsCreate.go @@ -0,0 +1,37 @@ +package tools + +import ( + "fmt" + "os" + + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +var file string + +var toolsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new private tool integration", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + name, id, err := createTool(file) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Succesfuly imported %s (%s)\n", name, id) + }, +} + +func init() { + ToolsCmd.AddCommand(toolsCreateCmd) + + toolsCreateCmd.Flags().StringVar(&file, "file", "", "YAML file for tool definition") + toolsCreateCmd.MarkFlagRequired("file") +} + +func createTool(fileName string) (string, uuid.UUID, error) { + return importTool(fileName, false) +} diff --git a/cmd/tools/toolsDelete.go b/cmd/tools/toolsDelete.go new file mode 100644 index 0000000..ad176ae --- /dev/null +++ b/cmd/tools/toolsDelete.go @@ -0,0 +1,64 @@ +package tools + +import ( + "fmt" + "net/http" + "os" + + "github.com/spf13/cobra" + "github.com/trickest/trickest-cli/client/request" +) + +var ( + toolID string + toolName string +) + +var toolsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a private tool integration", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + if toolName == "" && toolID == "" { + cmd.Help() + return + } + + if toolName != "" { + id, err := getToolIDByName(toolName) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + toolID = id.String() + } + + err := deleteTool(toolID) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Succesfuly deleted %s\n", toolID) + }, +} + +func init() { + ToolsCmd.AddCommand(toolsDeleteCmd) + + toolsDeleteCmd.Flags().StringVar(&toolID, "id", "", "ID of the tool to delete") + toolsDeleteCmd.Flags().StringVar(&toolName, "name", "", "Name of the tool to delete") +} + +func deleteTool(toolID string) error { + resp := request.Trickest.Delete().DoF("library/tool/%s/", toolID) + if resp == nil { + return fmt.Errorf("couldn't delete %s: invalid response", toolID) + } + + if resp.Status() == http.StatusNoContent { + return nil + } else { + return fmt.Errorf("couldn't delete %s: unexpected status code (%d)", toolID, resp.Status()) + } +} diff --git a/cmd/tools/toolsList.go b/cmd/tools/toolsList.go new file mode 100644 index 0000000..a072154 --- /dev/null +++ b/cmd/tools/toolsList.go @@ -0,0 +1,44 @@ +package tools + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/trickest/trickest-cli/cmd/library" +) + +var jsonOutput bool + +var toolsListCmd = &cobra.Command{ + Use: "list", + Short: "List private tool integrations", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + err := listTools(jsonOutput) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + }, +} + +func init() { + ToolsCmd.AddCommand(toolsListCmd) + + toolsListCmd.Flags().BoolVar(&jsonOutput, "json", false, "Display output in JSON format") +} + +func listTools(jsonOutput bool) error { + tools, err := ListPrivateTools("") + if err != nil { + return fmt.Errorf("couldn't list private tools: %w", err) + } + + if len(tools) == 0 { + return fmt.Errorf("couldn't find any private tools. Did you mean `library list tools`?") + } + + library.PrintTools(tools, jsonOutput) + return nil +} diff --git a/cmd/tools/toolsUpdate.go b/cmd/tools/toolsUpdate.go new file mode 100644 index 0000000..c94a7cc --- /dev/null +++ b/cmd/tools/toolsUpdate.go @@ -0,0 +1,35 @@ +package tools + +import ( + "fmt" + "os" + + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +var toolsUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update a private tool integration", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + name, id, err := updateTool(file) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Succesfuly imported %s (%s)\n", name, id) + }, +} + +func init() { + ToolsCmd.AddCommand(toolsUpdateCmd) + + toolsUpdateCmd.Flags().StringVar(&file, "file", "", "YAML file for tool definition") + toolsUpdateCmd.MarkFlagRequired("file") +} + +func updateTool(fileName string) (string, uuid.UUID, error) { + return importTool(fileName, true) +} diff --git a/go.mod b/go.mod index 9af48ce..0c41fd6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/trickest/trickest-cli go 1.19 require ( + github.com/go-yaml/yaml v2.1.0+incompatible github.com/google/uuid v1.3.0 github.com/gosuri/uilive v0.0.4 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b @@ -21,4 +22,5 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index c80ceb7..009b89f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY= @@ -41,6 +43,8 @@ golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/types/list.go b/types/list.go index a7dce6c..ddebd36 100644 --- a/types/list.go +++ b/types/list.go @@ -211,4 +211,22 @@ type ToolInput struct { Description string `json:"description"` Command string `json:"command"` Order int `json:"order"` + Visible bool `json:"visible"` +} + +type Categories struct { + Next any `json:"next"` + Previous any `json:"previous"` + Page int `json:"page"` + Last int `json:"last"` + Count int `json:"count"` + Results []Category `json:"results"` +} + +type Category struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WorkflowCount int `json:"workflow_count"` + ToolCount int `json:"tool_count"` } diff --git a/types/tools.go b/types/tools.go new file mode 100644 index 0000000..51e5ca0 --- /dev/null +++ b/types/tools.go @@ -0,0 +1,22 @@ +package types + +import "github.com/google/uuid" + +type ToolImportRequest struct { + VaultInfo uuid.UUID `json:"vault_info" yaml:"vault_info"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Category string `json:"tool_category_name" yaml:"category"` + CategoryID uuid.UUID `json:"tool_category" yaml:"tool_category"` + OutputCommand string `json:"output_command" yaml:"output_parameter"` + SourceURL string `json:"source_url" yaml:"source_url"` + DockerImage string `json:"docker_image" yaml:"docker_image"` + Command string `json:"command" yaml:"command"` + OutputType string `json:"output_type" yaml:"output_type"` + Inputs map[string]ToolInput `json:"inputs" yaml:"inputs"` + LicenseInfo struct { + Name string `json:"name" yaml:"name"` + Url string `json:"url" yaml:"url"` + } `json:"license_info" yaml:"license_info"` + DocLink string `json:"doc_link" yaml:"doc_link"` +} diff --git a/util/util.go b/util/util.go index b184d59..241b7e1 100644 --- a/util/util.go +++ b/util/util.go @@ -540,3 +540,42 @@ func DownloadFile(url, outputDir, fileName string) error { } return nil } + +func SearchLibraryCategories(name string) ([]types.Category, error) { + endpoint := "library/categories/" + if name != "" { + endpoint += "?name=" + name + } else { + endpoint += "?page_size=100" + } + + resp := request.Trickest.Get().Do(endpoint) + if resp == nil || resp.Status() != http.StatusOK { + request.ProcessUnexpectedResponse(resp) + } + + var categories types.Categories + err := json.Unmarshal(resp.Body(), &categories) + if err != nil { + return nil, fmt.Errorf("couldn't parse API response: %s", err) + } + + return categories.Results, nil +} + +func GetCategoryIDByName(name string) (uuid.UUID, error) { + categories, err := SearchLibraryCategories(name) + if err != nil { + return uuid.Nil, fmt.Errorf("couldn't search for %s: %w", name, err) + } + + if len(categories) == 0 { + return uuid.Nil, fmt.Errorf("couldn't find category '%s'", name) + } + + if len(categories) > 1 { + return uuid.Nil, fmt.Errorf("found more than one match for '%s'", name) + } + + return categories[0].ID, nil +}