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

Support merging JSON arrays, objects #148

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -3,6 +3,7 @@ module github.com/cli/go-gh/v2
go 1.21

require (
dario.cat/mergo v1.0.0
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/MakeNowJust/heredoc v1.0.0
github.com/charmbracelet/glamour v0.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
@@ -1,3 +1,5 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
Expand Down
76 changes: 76 additions & 0 deletions pkg/jsonmerge/array.go
@@ -0,0 +1,76 @@
package jsonmerge

import "io"

type arrayMerger struct {
isFirstPage bool
}

// NewArrayMerger creates a Merger for JSON arrays.
func NewArrayMerger() Merger {
return &arrayMerger{
isFirstPage: true,
}
}

func (merger *arrayMerger) NewPage(r io.Reader, isLastPage bool) io.ReadCloser {
return &arrayMergerPage{
merger: merger,
Reader: r,
isLastPage: isLastPage,
}
}

func (m *arrayMerger) Close() error {
// arrayMerger merges when reading, so any output was already written
// and there's nothing to do on Close.
return nil
}

type arrayMergerPage struct {
merger *arrayMerger

io.Reader
isLastPage bool

isSubsequentRead bool
cachedByte byte
}

func (page *arrayMergerPage) Read(p []byte) (int, error) {
var n int
var err error

if page.cachedByte != 0 && len(p) > 0 {
p[0] = page.cachedByte
n, err = page.Reader.Read(p[1:])
n += 1
page.cachedByte = 0
} else {
n, err = page.Reader.Read(p)
}

if !page.isSubsequentRead && !page.merger.isFirstPage && n > 0 && p[0] == '[' {
if n > 1 && p[1] == ']' {
// Empty array case.
p[0] = ' '
} else {
// Avoid starting a new array and continue with a comma instead.
p[0] = ','
}
}

if !page.isLastPage && n > 0 && p[n-1] == ']' {
// Avoid closing off an array in case we determine we are at EOF.
page.cachedByte = p[n-1]
n -= 1
}

page.isSubsequentRead = true
return n, err
}

func (page *arrayMergerPage) Close() error {
page.merger.isFirstPage = false
return nil
}
71 changes: 71 additions & 0 deletions pkg/jsonmerge/array_test.go
@@ -0,0 +1,71 @@
package jsonmerge

import (
"bytes"
"io"
"testing"

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

func TestArrayMerger_singleEmptyArray(t *testing.T) {
merger := NewArrayMerger()
w := &bytes.Buffer{}

r1 := bytes.NewBufferString(`[]`)
p1 := merger.NewPage(r1, true)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(2), n)
assert.NoError(t, p1.Close())
assert.Equal(t, `[]`, w.String())

require.NoError(t, merger.Close())
}

func TestArrayMerger_finalEmptyArray(t *testing.T) {
merger := NewArrayMerger()
w := &bytes.Buffer{}

r1 := bytes.NewBufferString(`["a","b"]`)
p1 := merger.NewPage(r1, false)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(8), n)
assert.NoError(t, p1.Close())
assert.Equal(t, `["a","b"`, w.String())

r2 := bytes.NewBufferString(`[]`)
p2 := merger.NewPage(r2, true)
n, err = io.Copy(w, p2)
require.NoError(t, err)
assert.Equal(t, int64(2), n)
assert.NoError(t, p2.Close())
assert.Equal(t, `["a","b" ]`, w.String())

require.NoError(t, merger.Close())
}

func TestArrayMerger_multiplePages(t *testing.T) {
merger := NewArrayMerger()
w := &bytes.Buffer{}

r1 := bytes.NewBufferString(`["a","b"]`)
p1 := merger.NewPage(r1, false)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(8), n)
assert.NoError(t, p1.Close())
assert.Equal(t, `["a","b"`, w.String())

r2 := bytes.NewBufferString(`["c","d"]`)
p2 := merger.NewPage(r2, true)
n, err = io.Copy(w, p2)
require.NoError(t, err)
assert.Equal(t, int64(9), n)
assert.NoError(t, p2.Close())
assert.Equal(t, `["a","b","c","d"]`, w.String())

require.NoError(t, merger.Close())
}
12 changes: 12 additions & 0 deletions pkg/jsonmerge/merge.go
@@ -0,0 +1,12 @@
// jsonmerge implements readers to merge JSON arrays or objects.
package jsonmerge

import (
"io"
)

// Merger is implemented to merge JSON arrays or objects.
type Merger interface {
NewPage(r io.Reader, isLastPage bool) io.ReadCloser
Close() error
}
73 changes: 73 additions & 0 deletions pkg/jsonmerge/object.go
@@ -0,0 +1,73 @@
package jsonmerge

import (
"bytes"
"encoding/json"
"io"

"dario.cat/mergo"
)

type objectMerger struct {
io.Writer
dst map[string]interface{}
}

// NewObjectMerger creates a Merger for JSON objects.
func NewObjectMerger(w io.Writer) Merger {
return &objectMerger{
Writer: w,
dst: make(map[string]interface{}),
}
}

func (merger *objectMerger) NewPage(r io.Reader, isLastPage bool) io.ReadCloser {
williammartin marked this conversation as resolved.
Show resolved Hide resolved
return &objectMergerPage{
merger: merger,
Reader: r,
}
}

func (merger *objectMerger) Close() error {
// Marshal to JSON and write to output.
buf, err := json.Marshal(merger.dst)
if err != nil {
return err
}

_, err = merger.Writer.Write(buf)
return err
}

type objectMergerPage struct {
merger *objectMerger

io.Reader
buffer bytes.Buffer
}

func (page *objectMergerPage) Read(p []byte) (int, error) {
heaths marked this conversation as resolved.
Show resolved Hide resolved
// Read into a temporary buffer to be merged and written later.
p = make([]byte, len(p))
heaths marked this conversation as resolved.
Show resolved Hide resolved
n, err := page.Reader.Read(p)
if err != nil {
return 0, err
}
if n == 0 {
heaths marked this conversation as resolved.
Show resolved Hide resolved
return 0, io.EOF
}

_, err = page.buffer.Write(p[:n])
return 0, err
}

func (page *objectMergerPage) Close() error {
var src map[string]interface{}

err := json.Unmarshal(page.buffer.Bytes(), &src)
if err != nil {
return err
heaths marked this conversation as resolved.
Show resolved Hide resolved
}

return mergo.Merge(&page.merger.dst, src, mergo.WithAppendSlice, mergo.WithOverride)
}
74 changes: 74 additions & 0 deletions pkg/jsonmerge/object_test.go
@@ -0,0 +1,74 @@
package jsonmerge

import (
"bytes"
"io"
"testing"

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

func TestObjectMerger_singleEmptyObject(t *testing.T) {
w := &bytes.Buffer{}
merger := NewObjectMerger(w)

r1 := bytes.NewBufferString(`{}`)
p1 := merger.NewPage(r1, true)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(0), n)
assert.NoError(t, p1.Close())
assert.Equal(t, ``, w.String())

require.NoError(t, merger.Close())
assert.JSONEq(t, `{}`, w.String())
}

func TestObjectMerger_finalEmptyObject(t *testing.T) {
w := &bytes.Buffer{}
merger := NewObjectMerger(w)

r1 := bytes.NewBufferString(`{"a":1,"b":2}`)
p1 := merger.NewPage(r1, false)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(0), n)
assert.NoError(t, p1.Close())
assert.Equal(t, ``, w.String())

r2 := bytes.NewBufferString(`{}`)
p2 := merger.NewPage(r2, true)
n, err = io.Copy(w, p2)
require.NoError(t, err)
assert.Equal(t, int64(0), n)
assert.NoError(t, p2.Close())
assert.Equal(t, ``, w.String())

require.NoError(t, merger.Close())
assert.JSONEq(t, `{"a":1,"b":2}`, w.String())
}

func TestObjectMerger_multiplePages(t *testing.T) {
w := &bytes.Buffer{}
merger := NewObjectMerger(w)

r1 := bytes.NewBufferString(`{"a":1,"b":2,"arr":["a","b"]}`)
p1 := merger.NewPage(r1, false)
n, err := io.Copy(w, p1)
require.NoError(t, err)
assert.Equal(t, int64(0), n)
assert.NoError(t, p1.Close())
assert.Equal(t, ``, w.String())

r2 := bytes.NewBufferString(`{"b":3,"c":{"d":4},"arr":["c","d"]}`)
p2 := merger.NewPage(r2, true)
n, err = io.Copy(w, p2)
require.NoError(t, err)
assert.Equal(t, int64(0), n)
assert.NoError(t, p2.Close())
assert.Equal(t, ``, w.String())

require.NoError(t, merger.Close())
assert.JSONEq(t, `{"a":1,"b":3,"c":{"d":4},"arr":["a","b","c","d"]}`, w.String())
}