Skip to content

Commit

Permalink
feat: GetInput improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgtaylor committed Oct 30, 2022
1 parent 1d4aa3a commit 4562674
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 41 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ $ j 'twitter: "@user"'

### Patch (Partial Update)

Partial updates are supported on existing data, which can be used to implement HTTP `PATCH`, templating, and other similar features. This feature combines the best of both:
Partial updates are supported on existing data, which can be used to implement HTTP `PATCH`, templating, and other similar features. The suggested content type for HTTP `PATCH` is `application/shorthand-patch`. This feature combines the best of both:

- [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386)
- [JSON Patch](https://www.rfc-editor.org/rfc/rfc6902)
Expand All @@ -331,6 +331,8 @@ Partial updates support:
- Moving/swapping fields or array items via `^`
- The right hand side is a path to the value to swap. See Querying below for the path syntax.

Note: When sending shorthand patches file loading via `@` should be disabled as the files will not exist on the server.

Some examples:

```sh
Expand Down
8 changes: 6 additions & 2 deletions cmd/j/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func main() {
cmd := &cobra.Command{
Use: fmt.Sprintf("%s [flags] key1: value1, key2: value2, ...", os.Args[0]),
Short: "Generate shorthand structured data",
Example: fmt.Sprintf("%s foo.bar: 1, .baz: true", os.Args[0]),
Example: fmt.Sprintf("%s foo{bar: 1, baz: true}", os.Args[0]),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 && *query == "" {
fmt.Println("At least one arg or --query need to be passed")
Expand All @@ -37,7 +37,7 @@ func main() {
}
fmt.Printf("Input: %s\n", strings.Join(args, " "))
}
result, err := shorthand.GetInputWithOptions(args, shorthand.ParseOptions{
result, isStructured, err := shorthand.GetInput(args, shorthand.ParseOptions{
EnableFileInput: true,
EnableObjectDetection: true,
ForceStringKeys: *format == "json",
Expand All @@ -51,6 +51,10 @@ func main() {
panic(err)
}
}
if !isStructured {
fmt.Println("Input file could not be parsed as structured data")
os.Exit(1)
}

if *query != "" {
if selected, ok, err := shorthand.GetPath(*query, result, shorthand.GetOptions{DebugLogger: debugLog}); ok {
Expand Down
50 changes: 30 additions & 20 deletions shorthand.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package shorthand

import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"sort"
"strings"

"gopkg.in/yaml.v3"
"unicode/utf8"
)

var ErrInvalidFile = errors.New("file cannot be parsed as structured data as it contains invalid UTF-8 characters")

func ConvertMapString(value any) any {
switch tmp := value.(type) {
case map[any]any:
Expand All @@ -33,41 +35,49 @@ func ConvertMapString(value any) any {
}

// GetInput loads data from stdin (if present) and from the passed arguments,
// returning the final structure.
func GetInput(args []string) (any, error) {
return GetInputWithOptions(args, ParseOptions{
EnableFileInput: true,
EnableObjectDetection: true,
})
}

func GetInputWithOptions(args []string, options ParseOptions) (any, error) {
// returning the final structure. Returns the result, whether the result is
// structured data (or raw file []byte), and if any errors occurred.
func GetInput(args []string, options ParseOptions) (any, bool, error) {
stat, _ := os.Stdin.Stat()
return getInput(stat.Mode(), os.Stdin, args, options)
}

func getInput(mode fs.FileMode, stdinFile io.Reader, args []string, options ParseOptions) (any, error) {
func getInput(mode fs.FileMode, stdinFile io.Reader, args []string, options ParseOptions) (any, bool, error) {
var stdin any

if (mode & os.ModeCharDevice) == 0 {
d, err := io.ReadAll(stdinFile)
if err != nil {
return nil, err
return nil, false, err
}

if err := yaml.Unmarshal(d, &stdin); err != nil {
if len(args) > 0 {
return nil, err
}
return nil, err
if len(args) == 0 {
// No modification requested, just pass the raw file through.
return d, false, nil
}

if !utf8.Valid(d) {
return nil, false, ErrInvalidFile
}

result, err := Unmarshal(string(d), ParseOptions{
EnableFileInput: options.EnableFileInput,
ForceStringKeys: options.ForceStringKeys,
ForceFloat64Numbers: options.ForceFloat64Numbers,
DebugLogger: options.DebugLogger,
}, nil)
if err != nil {
return nil, false, err
}
stdin = result
}

if len(args) == 0 {
return stdin, nil
return stdin, true, nil
}

return Unmarshal(strings.Join(args, " "), options, stdin)
result, err := Unmarshal(strings.Join(args, " "), options, stdin)
return result, true, err
}

func Unmarshal(input string, options ParseOptions, existing any) (any, Error) {
Expand Down
98 changes: 80 additions & 18 deletions shorthand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,95 @@ package shorthand

import (
"encoding/json"
"io"
"io/fs"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var getInputExamples = []struct {
Name string
Mode fs.FileMode
File io.Reader
Input string
JSON string
Output []byte
}{
{
Name: "No file",
Mode: fs.ModeCharDevice,
Input: "foo[]: 2, bar.another: false, existing: null, existing[]: 1",
JSON: `{
"foo": [2],
"bar": {
"another": false
},
"existing": [1]
}`,
},
{
Name: "Raw file",
File: strings.NewReader("a text file"),
Output: []byte("a text file"),
},
{
Name: "Structured file no args",
File: strings.NewReader(`{"foo":"bar"}`),
Output: []byte(`{"foo":"bar"}`),
},
{
Name: "JSON edit",
File: strings.NewReader(`{
"foo": [1],
"bar": {
"baz": true
},
"existing": [1, 2, 3]
}`),
Input: "foo[]: 2, bar.another: false, existing: null, existing[]: 1",
JSON: `{
"foo": [1, 2],
"bar": {
"another": false,
"baz": true
},
"existing": [1]
}`,
},
}

func TestGetInput(t *testing.T) {
file := strings.NewReader(`{
"foo": [1],
"bar": {
"baz": true
},
"existing": [1, 2, 3]
}`)
for _, example := range getInputExamples {
t.Run(example.Name, func(t *testing.T) {
input := []string{}
if example.Input != "" {
input = append(input, example.Input)
}
result, isStruct, err := getInput(example.Mode, example.File, input, ParseOptions{
EnableObjectDetection: true,
})
msg := ""
if e, ok := err.(Error); ok {
msg = e.Pretty()
}
require.NoError(t, err, msg)

result, err := getInput(0, file, []string{"foo[]: 2, bar.another: false, existing: null, existing[]: 1"}, ParseOptions{EnableObjectDetection: true})
assert.NoError(t, err)
if example.JSON != "" {
if !isStruct {
t.Fatal("input not recognized as structured data")
}
j, _ := json.Marshal(result)
assert.JSONEq(t, example.JSON, string(j))
}

j, _ := json.Marshal(result)
assert.JSONEq(t, `{
"foo": [1, 2],
"bar": {
"another": false,
"baz": true
},
"existing": [1]
}`, string(j))
if example.Output != nil {
assert.Equal(t, example.Output, result)
}
})
}
}

var marshalExamples = []struct {
Expand Down

0 comments on commit 4562674

Please sign in to comment.