diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e55cf63..cbc81a1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ * @openfga/dx -README.md @openfga/product @openfga/community +README.md @openfga/product @openfga/community @openfga/dx diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 0ab0ca7..8bfedea 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -111,7 +111,7 @@ jobs: check-latest: true go-version: ${{ env.GO_VERSION }} - - uses: anchore/sbom-action/download-syft@b6a39da80722a2cb0ef5d197531764a89b5d48c3 # v0.15.8 + - uses: anchore/sbom-action/download-syft@9fece9e20048ca9590af301449208b2b8861333b # v0.15.9 - name: Run GoReleaser uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 @@ -154,7 +154,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # v3.4.0 - - uses: anchore/sbom-action/download-syft@b6a39da80722a2cb0ef5d197531764a89b5d48c3 # v0.15.8 + - uses: anchore/sbom-action/download-syft@9fece9e20048ca9590af301449208b2b8861333b # v0.15.9 - name: Run GoReleaser uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 diff --git a/.golangci.yaml b/.golangci.yaml index 18e3d3a..e9a010d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -50,6 +50,7 @@ linters-settings: - google.golang.org/protobuf/types/known/structpb - gopkg.in/yaml.v3 - github.com/hashicorp/go-multierror + - github.com/gocarina/gocsv test: files: - "$test" diff --git a/cmd/tuple/read.go b/cmd/tuple/read.go index 175d0f9..f15240a 100644 --- a/cmd/tuple/read.go +++ b/cmd/tuple/read.go @@ -18,7 +18,9 @@ package tuple import ( "context" + "encoding/json" "fmt" + "strings" openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" @@ -35,6 +37,55 @@ type readResponse struct { complete *openfga.ReadResponse simple []openfga.TupleKey } +type readResponseCSVDTO struct { + UserType string `csv:"user_type"` + UserID string `csv:"user_id"` + UserRelation string `csv:"user_relation,omitempty"` + Relation string `csv:"relation"` + ObjectType string `csv:"object_type"` + ObjectID string `csv:"object_id"` + ConditionName string `csv:"condition_name,omitempty"` + ConditionContext string `csv:"condition_context,omitempty"` +} + +func (r readResponse) toCsvDTO() ([]readResponseCSVDTO, error) { + readResponseDTO := make([]readResponseCSVDTO, 0, len(r.simple)) + + for _, readRes := range r.simple { + // Handle Condition + conditionName := "" + conditionalContext := "" + + if readRes.Condition != nil { + conditionName = readRes.Condition.Name + + if readRes.Condition.Context != nil { + b, err := json.Marshal(readRes.Condition.Context) + if err != nil { + return nil, fmt.Errorf("failed to convert condition context to CSV: %w", err) + } + + conditionalContext = string(b) + } + } + // Split User and Object + user := strings.Split(readRes.User, ":") + object := strings.Split(readRes.Object, ":") + + // Append to DTO + readResponseDTO = append(readResponseDTO, readResponseCSVDTO{ + UserType: user[0], + UserID: user[1], + Relation: readRes.Relation, + ObjectType: object[0], + ObjectID: object[1], + ConditionName: conditionName, + ConditionContext: conditionalContext, + }) + } + + return readResponseDTO, nil +} func baseRead(fgaClient client.SdkClient, body *client.ClientReadRequest, maxPages int) ( *openfga.ReadResponse, error, @@ -126,11 +177,27 @@ var readCmd = &cobra.Command{ } simpleOutput, _ := cmd.Flags().GetBool("simple-output") - if simpleOutput { - return output.Display(response.simple) //nolint:wrapcheck + outputFormat, _ := cmd.Flags().GetString("output-format") + dataPrinter := output.NewUniPrinter(outputFormat) + + if outputFormat == "csv" { + data, _ := response.toCsvDTO() + + err := dataPrinter.Display(data) + if err != nil { + return fmt.Errorf("failed to display csv: %w", err) + } + + return nil + } + var data any + data = *response.complete + + if simpleOutput || outputFormat == "simple-json" { + data = response.simple } - return output.Display(*response.complete) //nolint:wrapcheck + return dataPrinter.Display(data) //nolint:wrapcheck }, } @@ -139,5 +206,9 @@ func init() { readCmd.Flags().String("relation", "", "Relation") readCmd.Flags().String("object", "", "Object") readCmd.Flags().Int("max-pages", MaxReadPagesLength, "Max number of pages to get. Set to 0 to get all pages.") - readCmd.Flags().Bool("simple-output", false, "Output simpler JSON version. (It can be used by write and delete commands)") //nolint:lll + readCmd.Flags().String("output-format", "json", "Specifies the format for data presentation. Valid options: "+ + "json, simple-json, csv, and yaml.") + readCmd.Flags().Bool("simple-output", false, "Output data in simpler version. (It can be used by write and delete commands)") //nolint:lll + _ = readCmd.Flags().MarkDeprecated("simple-output", "the flag \"simple-output\" is deprecated and will be removed"+ + " in future releases.\nPlease use the \"--output-format=simple-json\" flag instead.") } diff --git a/cmd/tuple/read_test.go b/cmd/tuple/read_test.go index ab280e4..4c82373 100644 --- a/cmd/tuple/read_test.go +++ b/cmd/tuple/read_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/golang/mock/gomock" openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" @@ -373,3 +375,61 @@ func TestReadMultiPagesMaxLimit(t *testing.T) { t.Errorf("Expected output %v actual %v", simpleOutput, string(simpleTxt)) } } + +func TestReadResponseCSVDTOParser(t *testing.T) { + t.Parallel() + + testCases := []struct { + readRes readResponse + expected []readResponseCSVDTO + }{ + { + readRes: readResponse{ + simple: []openfga.TupleKey{ + { + User: "user:anne", + Relation: "reader", + Object: "document:secret.doc", + Condition: &openfga.RelationshipCondition{ + Name: "inOfficeIP", + Context: toPointer(map[string]interface{}{"ip_addr": "10.0.0.1"}), + }, + }, + { + User: "user:john", + Relation: "writer", + Object: "document:abc.doc", + Condition: &openfga.RelationshipCondition{}, + }, + }, + }, + expected: []readResponseCSVDTO{ + { + UserType: "user", + UserID: "anne", + Relation: "reader", + ObjectType: "document", + ObjectID: "secret.doc", + ConditionName: "inOfficeIP", + ConditionContext: "{\"ip_addr\":\"10.0.0.1\"}", + }, + { + UserType: "user", + UserID: "john", + Relation: "writer", + ObjectType: "document", + ObjectID: "abc.doc", + }, + }, + }, + } + + for _, testCase := range testCases { + outcome, _ := testCase.readRes.toCsvDTO() + assert.Equal(t, testCase.expected, outcome) + } +} + +func toPointer[T any](p T) *T { + return &p +} diff --git a/go.mod b/go.mod index 358bbcb..6359744 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openfga/cli go 1.21.8 require ( + github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a github.com/golang/mock v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/mattn/go-isatty v0.0.20 @@ -34,7 +35,7 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.20.0 // indirect + github.com/google/cel-go v0.20.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect @@ -52,8 +53,8 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.49.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/common v0.50.0 // indirect + github.com/prometheus/procfs v0.13.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -76,8 +77,8 @@ require ( golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect google.golang.org/grpc v1.62.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 053c0f1..127222d 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA= +github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -67,8 +69,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.20.0 h1:h4n6DOCppEMpWERzllyNkntl7JrDyxoE543KWS6BLpc= -github.com/google/cel-go v0.20.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -153,14 +155,8 @@ github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58 github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/openfga/api/proto v0.0.0-20240312180017-0c609904ae24 h1:1mFm89i8/yguJf3VQYjpy+Pbv0MGf/Vxp4KoEFzKQSw= github.com/openfga/api/proto v0.0.0-20240312180017-0c609904ae24/go.mod h1:5LtWOArDX4FlbcfDvBoJAzDEYJKLz/OEUoi+0S2tyM8= -github.com/openfga/go-sdk v0.3.5 h1:KQXhMREh+g/K7HNuZ/YmXuHkREkq0VMKteua4bYr3Uw= -github.com/openfga/go-sdk v0.3.5/go.mod h1:u1iErzj5E9/bhe+8nsMv0gigcYbJtImcdgcE5DmpbBg= github.com/openfga/go-sdk v0.3.6-0.20240313140700-3de2c059df44 h1:JyFxbUvKEqMnCoJL4QoH70kpBxkFaHtmo4tFNKIsfoo= github.com/openfga/go-sdk v0.3.6-0.20240313140700-3de2c059df44/go.mod h1:dybCHDtJDwkmtlxxtgQrHR2c8HPVOwN493drbXv46Ec= -github.com/openfga/language/pkg/go v0.0.0-20240312222412-7d8ae9182e09 h1:zFyslv7PQyAImXh/2NqcfEGZfmMopf6sTk+OZygfPnw= -github.com/openfga/language/pkg/go v0.0.0-20240312222412-7d8ae9182e09/go.mod h1:iwtNOC/ypBBmN4ND4JqtLdgskIEN67GRP6HuTVa0dKE= -github.com/openfga/language/pkg/go v0.0.0-20240312223328-605a55c5f880 h1:lqPDk9xsY8NYxfaxjj66Dj/yThVGrbp8tWVgHDYVrD4= -github.com/openfga/language/pkg/go v0.0.0-20240312223328-605a55c5f880/go.mod h1:iwtNOC/ypBBmN4ND4JqtLdgskIEN67GRP6HuTVa0dKE= github.com/openfga/language/pkg/go v0.0.0-20240313142251-ccdeba043413 h1:EsXzazcekc+E+PzBCp2RGN8O9xiPBWcN//12p+aSsVk= github.com/openfga/language/pkg/go v0.0.0-20240313142251-ccdeba043413/go.mod h1:iwtNOC/ypBBmN4ND4JqtLdgskIEN67GRP6HuTVa0dKE= github.com/openfga/openfga v1.5.1-0.20240312222040-3f13843536d9 h1:hzvo+cCWhCrzHNlAZcsYD/kqem6qKw6NZOR9xE8xLrw= @@ -182,10 +178,10 @@ github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdU github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= -github.com/prometheus/common v0.49.0 h1:ToNTdK4zSnPVJmh698mGFkDor9wBI/iGaJy5dbH1EgI= -github.com/prometheus/common v0.49.0/go.mod h1:Kxm+EULxRbUkjGU6WFsQqo3ORzB4tyKvlWFOE9mB2sE= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= +github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -335,10 +331,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/internal/output/marshal.go b/internal/output/marshal.go index 70d4553..828d0ad 100644 --- a/internal/output/marshal.go +++ b/internal/output/marshal.go @@ -22,10 +22,127 @@ import ( "fmt" "os" + "github.com/gocarina/gocsv" + "gopkg.in/yaml.v3" + "github.com/mattn/go-isatty" "github.com/nwidger/jsoncolor" ) +// Printer is a content type agnostic interface for displaying data. +type Printer interface { + DisplayNoColor(data any) error + DisplayColor(data any) error +} + +// jsonPrinter implements the Printer interface for JSON output. +type jsonPrinter struct{} + +// csvPrinter implements the Printer interface for CSV output. +type csvPrinter struct{} + +// yamlPrinter implements the Printer interface for YAML output. +type yamlPrinter struct{} + +func (prt *jsonPrinter) DisplayNoColor(data any) error { + result, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal json with error %w", err) + } + + fmt.Println(string(result)) + + return nil +} + +func (prt *jsonPrinter) DisplayColor(data any) error { + // create custom formatter + f := jsoncolor.NewFormatter() + + dst, err := jsoncolor.MarshalIndentWithFormatter(data, "", " ", f) + if err != nil { + return fmt.Errorf("unable to display output with error %w", err) + } + + fmt.Println(string(dst)) + + return nil +} + +func (prt *csvPrinter) DisplayColor(data any) error { + return prt.DisplayNoColor(data) +} + +func (prt *csvPrinter) DisplayNoColor(data any) error { + b, err := gocsv.MarshalBytes(data) + if err != nil { + return fmt.Errorf("unable to marshal CSV with error: %w", err) + } + + fmt.Println(string(b)) + + return nil +} + +func (prt *yamlPrinter) DisplayColor(data any) error { + return prt.DisplayNoColor(data) +} + +func (prt *yamlPrinter) DisplayNoColor(data any) error { + result, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("unable to marshal yaml with error %w", err) + } + + fmt.Println(string(result)) + + return nil +} + +// UniPrinter is a universal printer that can handle different output formats. +type UniPrinter struct { + Colorful bool + Printer Printer +} + +// NewUniPrinter creates a new UniPrinter based on the specified output format and optional functional options. +func NewUniPrinter(outputFormat string) *UniPrinter { + uniPrinter := UniPrinter{Colorful: true} + if os.Getenv("NO_COLOR") != "" { + uniPrinter.Colorful = false + } + + switch outputFormat { + case "yaml": + uniPrinter.Printer = &yamlPrinter{} + case "csv": + uniPrinter.Printer = &csvPrinter{} + default: + uniPrinter.Printer = &jsonPrinter{} + } + + return &uniPrinter +} + +// Display prints the data using the configured printer and color settings. +func (prt UniPrinter) Display(data any) error { + if prt.Colorful { + err := prt.Printer.DisplayColor(data) + if err != nil { + return fmt.Errorf("failed to display colorful output: %w", err) + } + + return nil + } + + err := prt.Printer.DisplayNoColor(data) + if err != nil { + return fmt.Errorf("failed to display output: %w", err) + } + + return nil +} + // EmptyStruct is used when we wish to return an empty object. type EmptyStruct struct{}