Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --template and support --format go-template=TEMPLATE experience #1377

Merged
merged 35 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d956bca
feat: redefine go template experience
qweeah May 7, 2024
bb9e950
add default value
qweeah May 7, 2024
477063e
update default
qweeah May 7, 2024
241c1b2
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 8, 2024
05fc9a8
fix: discover init error
qweeah May 8, 2024
2d86e91
add tests
qweeah May 8, 2024
b65dd71
fix e2e
qweeah May 8, 2024
799e313
add unit test
qweeah May 9, 2024
467ab8e
code clean
qweeah May 9, 2024
4f0334c
make ut easier
qweeah May 9, 2024
ddb9b0a
try make UT easier
qweeah May 9, 2024
97b8cc0
add unit tests
qweeah May 9, 2024
ae6b4f2
code clean
qweeah May 9, 2024
10510b7
bug fix && add e2e
qweeah May 9, 2024
69afad6
bug fix
qweeah May 9, 2024
e0eb0fb
remove focus
qweeah May 9, 2024
10b7016
revert unnecessary change
qweeah May 10, 2024
53392e2
code clean
qweeah May 10, 2024
0014f3a
code clean
qweeah May 11, 2024
6db1020
code clean
qweeah May 11, 2024
1224dd2
add experimental to template flag
qweeah May 11, 2024
15647d0
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 13, 2024
74522e5
resolve comments
qweeah May 13, 2024
91584be
add flushing
qweeah May 13, 2024
c1dfafd
add e2e
qweeah May 13, 2024
aa60110
refine recommendation
qweeah May 13, 2024
927573a
Merge remote-tracking branch 'upstream/main' into format-template
qweeah May 13, 2024
5c53b24
add default format help doc
qweeah May 13, 2024
cc2c8e3
fix e2e
qweeah May 13, 2024
364729f
code clean
qweeah May 13, 2024
e571ad2
resolve nit comment
qweeah May 13, 2024
bc81c38
recommend only primary usage
qweeah May 13, 2024
61c5f1f
correct description
qweeah May 13, 2024
47c35ec
fix build error
qweeah May 13, 2024
70cd7f0
add parameter to format type
qweeah May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 46 additions & 33 deletions cmd/oras/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,120 +30,133 @@
"oras.land/oras/cmd/oras/internal/display/metadata/text"
"oras.land/oras/cmd/oras/internal/display/metadata/tree"
"oras.land/oras/cmd/oras/internal/display/status"
"oras.land/oras/cmd/oras/internal/option"
)

// NewPushHandler returns status and metadata handlers for push command.
func NewPushHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler) {
func NewPushHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler, error) {
var statusHandler status.PushHandler
if tty != nil {
statusHandler = status.NewTTYPushHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextPushHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.PushHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewPushHandler(out)
case "json":
case option.FormatTypeJSON:
metadataHandler = json.NewPushHandler(out)
case option.FormatTypeGoTemplate:
metadataHandler = template.NewPushHandler(out, format.Template)
default:
metadataHandler = template.NewPushHandler(out, format)
return nil, nil, format.TypeError()
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewAttachHandler returns status and metadata handlers for attach command.
func NewAttachHandler(out io.Writer, format string, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler) {
func NewAttachHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler, error) {
var statusHandler status.AttachHandler
if tty != nil {
statusHandler = status.NewTTYAttachHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextAttachHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.AttachHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewAttachHandler(out)
case "json":
case option.FormatTypeJSON:
metadataHandler = json.NewAttachHandler(out)
case option.FormatTypeGoTemplate:
metadataHandler = template.NewAttachHandler(out, format.Template)
default:
metadataHandler = template.NewAttachHandler(out, format)
return nil, nil, format.TypeError()
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewPullHandler returns status and metadata handlers for pull command.
func NewPullHandler(out io.Writer, format string, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler) {
func NewPullHandler(out io.Writer, format option.Format, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler, error) {
var statusHandler status.PullHandler
if tty != nil {
statusHandler = status.NewTTYPullHandler(tty)
} else if format == "" {
} else if format.Type == "" {
statusHandler = status.NewTextPullHandler(out, verbose)
} else {
statusHandler = status.NewDiscardHandler()
}

var metadataHandler metadata.PullHandler
switch format {
switch format.Type {
case "":
metadataHandler = text.NewPullHandler(out)
case "json":
case option.FormatTypeJSON:
metadataHandler = json.NewPullHandler(out, path)
case option.FormatTypeGoTemplate:
metadataHandler = template.NewPullHandler(out, path, format.Template)
default:
metadataHandler = template.NewPullHandler(out, path, format)
return nil, nil, format.TypeError()
}
return statusHandler, metadataHandler
return statusHandler, metadataHandler, nil
}

// NewDiscoverHandler returns status and metadata handlers for discover command.
func NewDiscoverHandler(out io.Writer, outputType string, path string, rawReference string, desc ocispec.Descriptor, verbose bool) metadata.DiscoverHandler {
switch outputType {
case "tree", "":
return tree.NewDiscoverHandler(out, path, desc, verbose)
case "table":
return table.NewDiscoverHandler(out, rawReference, desc, verbose)
case "json":
return json.NewDiscoverHandler(out, desc, path)
func NewDiscoverHandler(out io.Writer, format option.Format, path string, rawReference string, desc ocispec.Descriptor, verbose bool) (metadata.DiscoverHandler, error) {
var handler metadata.DiscoverHandler
switch format.Type {
case option.FormatTypeTree, "":
handler = tree.NewDiscoverHandler(out, path, desc, verbose)
case option.FormatTypeTable:
handler = table.NewDiscoverHandler(out, rawReference, desc, verbose)
case option.FormatTypeJSON:
handler = json.NewDiscoverHandler(out, desc, path)
case option.FormatTypeGoTemplate:
handler = template.NewDiscoverHandler(out, desc, path, format.Template)
default:
return template.NewDiscoverHandler(out, desc, path, outputType)
return nil, format.TypeError()

Check warning on line 124 in cmd/oras/internal/display/handler.go

View check run for this annotation

Codecov / codecov/patch

cmd/oras/internal/display/handler.go#L124

Added line #L124 was not covered by tests
}
return handler, nil
}

// NewManifestFetchHandler returns a manifest fetch handler.
func NewManifestFetchHandler(out io.Writer, format string, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler) {
func NewManifestFetchHandler(out io.Writer, format option.Format, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler, error) {
var metadataHandler metadata.ManifestFetchHandler
var contentHandler content.ManifestFetchHandler

switch format {
switch format.Type {
case "":
// raw
if outputDescriptor {
metadataHandler = descriptor.NewManifestFetchHandler(out, pretty)
} else {
metadataHandler = metadata.NewDiscardHandler()
}
case "json":
case option.FormatTypeJSON:
// json
metadataHandler = json.NewManifestFetchHandler(out)
if outputPath == "" {
contentHandler = content.NewDiscardHandler()
}
default:
case option.FormatTypeGoTemplate:
// go template
metadataHandler = template.NewManifestFetchHandler(out, format)
metadataHandler = template.NewManifestFetchHandler(out, format.Template)
if outputPath == "" {
contentHandler = content.NewDiscardHandler()
}
default:
return nil, nil, format.TypeError()
}

if contentHandler == nil {
contentHandler = content.NewManifestFetchHandler(out, pretty, outputPath)
}
return metadataHandler, contentHandler
return metadataHandler, contentHandler, nil
}
126 changes: 116 additions & 10 deletions cmd/oras/internal/option/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,127 @@ limitations under the License.

package option

import "github.com/spf13/pflag"
import (
"fmt"
"strings"

// Format is a flag to format metadata into output.
"github.com/spf13/cobra"
"github.com/spf13/pflag"
oerrors "oras.land/oras/cmd/oras/internal/errors"
)

const (
// format types
FormatTypeJSON = "json"
FormatTypeTree = "tree"
FormatTypeTable = "table"
FormatTypeGoTemplate = "go-template"
)
qweeah marked this conversation as resolved.
Show resolved Hide resolved

// FormatType represents a custom description in help doc.
type FormatType struct {
Name string
Usage string
}

// Format contains input and parsed options for formatted output flags.
type Format struct {
Type string
Template string
// FormatFlag can be private once deprecated `--output` is removed from
// `oras discover`
FormatFlag string
types []FormatType
}

// ApplyFlag implements FlagProvider.ApplyFlag.
func (opts *Format) ApplyFlags(fs *pflag.FlagSet) {
const name = "format"
if fs.Lookup(name) != nil {
// allow command to overwrite the flag
return
}
fs.StringVar(&opts.Template, name, "", `[Experimental] Format output using a custom template:
'json': Print in JSON format
'$TEMPLATE': Print output using the given Go template.`)
usage := "[Experimental] Format output using a custom template:"
if len(opts.types) == 0 {
opts.types = []FormatType{
{Name: FormatTypeJSON, Usage: "Print in JSON format"},
{Name: FormatTypeGoTemplate, Usage: "Print output using the given Go template"},
}
}

// generate usage string
maxLength := 0
for _, option := range opts.types {
if len(option.Name) > maxLength {
maxLength = len(option.Name)
}
}
for _, option := range opts.types {
usage += fmt.Sprintf("\n'%s':%s%s", option.Name, strings.Repeat(" ", maxLength-len(option.Name)+2), option.Usage)
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved

// apply flags
fs.StringVar(&opts.FormatFlag, "format", opts.FormatFlag, usage)
fs.StringVar(&opts.Template, "template", "", "[Experimental] Template string used to format output")
}

// Parse parses the input format flag.
func (opts *Format) Parse(_ *cobra.Command) error {
if err := opts.parseFlag(); err != nil {
return err
}

if opts.Type == "" {
// flag not specified
return nil
}

var optionalTypes []string
for _, option := range opts.types {
if opts.Type == option.Name {
// type validation passed
return nil
}
optionalTypes = append(optionalTypes, option.Name)
}
return &oerrors.Error{
Err: fmt.Errorf("invalid format type: %q", opts.Type),
Recommendation: fmt.Sprintf("supported types: %s", strings.Join(optionalTypes, ", ")),
}
}

func (opts *Format) parseFlag() error {
opts.Type = opts.FormatFlag
if opts.Template != "" {
// template explicitly set
if opts.Type != FormatTypeGoTemplate {
return fmt.Errorf("--template must be used with --format %s", FormatTypeGoTemplate)
qweeah marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}

goTemplatePrefix := FormatTypeGoTemplate + "="
if strings.HasPrefix(opts.FormatFlag, goTemplatePrefix) {
// add parameter to template
opts.Type = FormatTypeGoTemplate
opts.Template = opts.FormatFlag[len(goTemplatePrefix):]
}
return nil
qweeah marked this conversation as resolved.
Show resolved Hide resolved
}

// SetTypes resets the format options and default value.
func (opts *Format) SetTypes(types []FormatType) {
opts.types = types
}

// SetTypesAndDefault resets the format options and default value.
// Caller should make sure that this function is used before applying flags.
func (opts *Format) SetTypesAndDefault(defaultType string, types []FormatType) {
opts.FormatFlag = defaultType
opts.types = types
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved

// FormatError generates the error message for an invalid type.
func (opts *Format) TypeError() error {
return fmt.Errorf("unsupported format type: %s", opts.Type)
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved

// RawFormatFlag returns raw input of --format flag.
func (opts *Format) RawFormatFlag() string {
return opts.FormatFlag
}
qweeah marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion cmd/oras/root/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder

func runAttach(cmd *cobra.Command, opts *attachOptions) error {
ctx, logger := command.GetLogger(cmd, &opts.Common)
displayStatus, displayMetadata, err := display.NewAttachHandler(cmd.OutOrStdout(), opts.Format, opts.TTY, opts.Verbose)
if err != nil {
return err
}

annotations, err := opts.LoadManifestAnnotations()
if err != nil {
return err
Expand All @@ -119,7 +124,6 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error {
Recommendation: `To attach to an existing artifact, please provide files via argument or annotations via flag "--annotation". Run "oras attach -h" for more options and examples`,
}
}
displayStatus, displayMetadata := display.NewAttachHandler(cmd.OutOrStdout(), opts.Template, opts.TTY, opts.Verbose)

// prepare manifest
store, err := file.New("")
Expand Down
42 changes: 42 additions & 0 deletions cmd/oras/root/attach_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
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 root

import (
"context"
"testing"

"github.com/spf13/cobra"
"oras.land/oras/cmd/oras/internal/option"
)

func Test_runAttach_errType(t *testing.T) {
// prpare
cmd := &cobra.Command{}
cmd.SetContext(context.Background())

// test
opts := &attachOptions{
Format: option.Format{
Type: "unknown",
},
}
got := runAttach(cmd, opts).Error()
want := opts.TypeError().Error()
if got != want {
t.Fatalf("got %v, want %v", got, want)
}
}
Loading
Loading