Repository: oras-project/oras
Pull request #536 (2022-09-06 07:39:48)

feat: add Descriptor and Pretty options for ORAS command
Add Descriptor and Pretty options for ORAS command so that these options could be used later in push/fetch/delete commands.

Signed-off-by: Zoey Li <zoeyli@microsoft.com>

codecov-commenter: # [Codecov](https://codecov.io/gh/oras-project/oras/pull/536?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) Report
> Merging [#536](https://codecov.io/gh/oras-project/oras/pull/536?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) (ed707aa) into [main](https://codecov.io/gh/oras-project/oras/commit/f4a31ec1b9824899bb382d82be7b291555622932?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) (f4a31ec) will **decrease** coverage by `0.16%`.
> The diff coverage is `70.00%`.

```diff
@@            Coverage Diff             @@
##             main     #536      +/-   ##
==========================================
- Coverage   73.43%   73.26%   -0.17%     
==========================================
  Files           9       11       +2     
  Lines         399      419      +20     
==========================================
+ Hits          293      307      +14     
- Misses         85       89       +4     
- Partials       21       23       +2     
```


| [Impacted Files](https://codecov.io/gh/oras-project/oras/pull/536?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project) | Coverage Δ | |
|---|---|---|
| [cmd/oras/internal/option/descriptor.go](https://codecov.io/gh/oras-project/oras/pull/536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project#diff-Y21kL29yYXMvaW50ZXJuYWwvb3B0aW9uL2Rlc2NyaXB0b3IuZ28=) | `62.50% <62.50%> (ø)` | |
| [cmd/oras/internal/option/pretty.go](https://codecov.io/gh/oras-project/oras/pull/536/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project#diff-Y21kL29yYXMvaW50ZXJuYWwvb3B0aW9uL3ByZXR0eS5nbw==) | `75.00% <75.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oras-project)


Patch:
diff --git a/cmd/oras/internal/option/descriptor.go b/cmd/oras/internal/option/descriptor.go
new file mode 100644
index 0000000..20017d4
--- /dev/null
+++ b/cmd/oras/internal/option/descriptor.go
@@ -0,0 +1,43 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package option
+
+import (
+	"encoding/json"
+	"fmt"
+
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/spf13/pflag"
+)
+
+// Descriptor option struct.
+type Descriptor struct {
+	OutputDescriptor bool
+}
+
+// ApplyFlags applies flags to a command flag set.
+func (opts *Descriptor) ApplyFlags(fs *pflag.FlagSet) {
+	fs.BoolVarP(&opts.OutputDescriptor, "descriptor", "", false, "output the descriptor")
+}
+
+// Marshal returns the JSON encoding of descriptor.
+func (opts *Descriptor) Marshal(desc ocispec.Descriptor) ([]byte, error) {
+	b, err := json.Marshal(desc)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal descriptor: %w", err)
+	}
+	return b, nil
+}
diff --git a/cmd/oras/internal/option/pretty.go b/cmd/oras/internal/option/pretty.go
new file mode 100644
index 0000000..0e75244
--- /dev/null
+++ b/cmd/oras/internal/option/pretty.go
@@ -0,0 +1,51 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package option
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+
+	"github.com/spf13/pflag"
+)
+
+// Pretty option struct.
+type Pretty struct {
+	pretty bool
+}
+
+// ApplyFlags applies flags to a command flag set.
+func (opts *Pretty) ApplyFlags(fs *pflag.FlagSet) {
+	fs.BoolVarP(&opts.pretty, "pretty", "", false, "prettify JSON objects printed to stdout")
+}
+
+// Output outputs the prettified content if `--pretty` flag is used. Otherwise
+// outputs the original content.
+func (opts *Pretty) Output(w io.Writer, content []byte) error {
+	if opts.pretty {
+		buf := bytes.NewBuffer(nil)
+		if err := json.Indent(buf, content, "", "  "); err != nil {
+			return fmt.Errorf("failed to prettify: %w", err)
+		}
+		buf.WriteByte('\n')
+		content = buf.Bytes()
+	}
+
+	_, err := w.Write(content)
+	return err
+}


Test patch:
diff --git a/cmd/oras/internal/option/descriptor_test.go b/cmd/oras/internal/option/descriptor_test.go
new file mode 100644
index 0000000..ca17e09
--- /dev/null
+++ b/cmd/oras/internal/option/descriptor_test.go
@@ -0,0 +1,59 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package option
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/spf13/pflag"
+)
+
+func TestDescriptor_ApplyFlags(t *testing.T) {
+	var test struct{ Descriptor }
+	ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError))
+	if test.Descriptor.OutputDescriptor != false {
+		t.Fatalf("expecting OutputDescriptor to be false but got: %v", test.Descriptor.OutputDescriptor)
+	}
+}
+
+func TestDescriptor_Marshal(t *testing.T) {
+	// generate test content
+	blob := []byte("hello world")
+	desc := ocispec.Descriptor{
+		MediaType: "test",
+		Digest:    digest.FromBytes(blob),
+		Size:      int64(len(blob)),
+	}
+	want, err := json.Marshal(desc)
+	if err != nil {
+		t.Fatal("error calling json.Marshal(), error =", err)
+	}
+
+	opts := Descriptor{
+		OutputDescriptor: true,
+	}
+	got, err := opts.Marshal(desc)
+	if err != nil {
+		t.Fatal("Descriptor.Marshal() error =", err)
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("Descriptor.Marshal() got %v, want %v", got, want)
+	}
+}
diff --git a/cmd/oras/internal/option/pretty_test.go b/cmd/oras/internal/option/pretty_test.go
new file mode 100644
index 0000000..5d00490
--- /dev/null
+++ b/cmd/oras/internal/option/pretty_test.go
@@ -0,0 +1,106 @@
+/*
+Copyright The ORAS Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package option
+
+import (
+	"encoding/json"
+	"io"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+
+	"github.com/opencontainers/go-digest"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/spf13/pflag"
+)
+
+func TestPretty_ApplyFlags(t *testing.T) {
+	var test struct{ Pretty }
+	ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError))
+	if test.Pretty.pretty != false {
+		t.Fatalf("expecting pretty to be false but got: %v", test.Pretty.pretty)
+	}
+}
+
+func TestPretty_Output(t *testing.T) {
+	// generate test content
+	blob := []byte("hello world")
+	desc := ocispec.Descriptor{
+		MediaType: "test",
+		Digest:    digest.FromBytes(blob),
+		Size:      int64(len(blob)),
+	}
+	want, err := json.Marshal(desc)
+	if err != nil {
+		t.Fatal("error calling json.Marshal(), error =", err)
+	}
+
+	tempDir := t.TempDir()
+	fileName := "test.txt"
+	path := filepath.Join(tempDir, fileName)
+	fp, err := os.Create(path)
+	if err != nil {
+		t.Fatal("error calling os.Create(), error =", err)
+	}
+	defer fp.Close()
+
+	// outputs unprettified content
+	opts := Pretty{
+		pretty: false,
+	}
+	err = opts.Output(fp, want)
+	if err != nil {
+		t.Fatal("Pretty.Output() error =", err)
+	}
+	if _, err = fp.Seek(0, io.SeekStart); err != nil {
+		t.Fatal("error calling File.Seek(), error =", err)
+	}
+	got, err := io.ReadAll(fp)
+	if err != nil {
+		t.Fatal("error calling io.ReadAll(), error =", err)
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("Pretty.Output() got %v, want %v", got, want)
+	}
+
+	// remove all content in the file
+	if err := os.Truncate(path, 0); err != nil {
+		t.Fatal("error calling os.Truncate(), error =", err)
+	}
+	if _, err = fp.Seek(0, io.SeekStart); err != nil {
+		t.Fatal("error calling File.Seek(), error =", err)
+	}
+
+	// outputs prettified content
+	opts = Pretty{
+		pretty: true,
+	}
+	err = opts.Output(fp, want)
+	if err != nil {
+		t.Fatal("Pretty.Output() error =", err)
+	}
+	if _, err = fp.Seek(0, io.SeekStart); err != nil {
+		t.Fatal("error calling File.Seek(), error =", err)
+	}
+	got, err = io.ReadAll(fp)
+	if err != nil {
+		t.Fatal("error calling io.ReadAll(), error =", err)
+	}
+	if reflect.DeepEqual(got, want) {
+		t.Fatalf("Pretty.Output() failed to prettified the content: %v", got)
+	}
+}