merger.go

  1package jsons
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"io"
  7	"strings"
  8
  9	"github.com/qjebbs/go-jsons/merge"
 10	"github.com/qjebbs/go-jsons/rule"
 11)
 12
 13// Merger is the json merger
 14type Merger struct {
 15	loadersByName map[Format]*loader
 16	loadersByExt  map[string]*loader
 17	rule          *rule.Rule
 18}
 19
 20// NewMerger returns a new Merger
 21func NewMerger(fields ...rule.Field) *Merger {
 22	m := &Merger{
 23		loadersByName: make(map[Format]*loader),
 24		loadersByExt:  make(map[string]*loader),
 25		rule:          rule.NewRule(fields...),
 26	}
 27	must(m.RegisterLoader(
 28		FormatJSON,
 29		[]string{".json"},
 30		func(v []byte) (map[string]interface{}, error) {
 31			m := make(map[string]interface{})
 32			if err := json.Unmarshal(v, &m); err != nil {
 33				return nil, err
 34			}
 35			return m, nil
 36		},
 37	))
 38	return m
 39}
 40
 41// Merge merges inputs into a single json.
 42//
 43// It detects the format by file extension, or try all mergers
 44// if no extension found
 45//
 46// Accepted Input:
 47//
 48//   - `string`: path to a local file
 49//   - `[]string`: paths of local files
 50//   - `[]byte`: content of a file
 51//   - `[][]byte`: content list of files
 52//   - `io.Reader`: content reader
 53//   - `[]io.Reader`: content readers
 54func (m *Merger) Merge(inputs ...interface{}) ([]byte, error) {
 55	target := make(map[string]interface{})
 56	for _, input := range inputs {
 57		err := m.mergeToMap(input, target)
 58		if err != nil {
 59			return nil, err
 60		}
 61	}
 62	err := m.rule.Apply(target)
 63	if err != nil {
 64		return nil, err
 65	}
 66	return json.Marshal(target)
 67}
 68
 69// MergeAs loads inputs of the specific format and merges into a single json.
 70//
 71// Accepted Input:
 72//
 73//   - `string`: path to a local file
 74//   - `[]string`: paths of local files
 75//   - `[]byte`: content of a file
 76//   - `[][]byte`: content list of files
 77//   - `io.Reader`: content reader
 78//   - `[]io.Reader`: content readers
 79func (m *Merger) MergeAs(format Format, inputs ...interface{}) ([]byte, error) {
 80	target := make(map[string]interface{})
 81	for _, input := range inputs {
 82		err := m.mergeToMapAs(format, input, target)
 83		if err != nil {
 84			return nil, err
 85		}
 86	}
 87	err := m.rule.Apply(target)
 88	if err != nil {
 89		return nil, err
 90	}
 91	return json.Marshal(target)
 92}
 93
 94func (m *Merger) mergeToMapAs(formatName Format, input interface{}, target map[string]interface{}) error {
 95	if formatName == FormatAuto {
 96		return m.mergeToMap(input, target)
 97	}
 98	f, found := m.loadersByName[formatName]
 99	if !found {
100		return fmt.Errorf("unknown format: %s", formatName)
101	}
102	maps, err := f.Load(input)
103	if err != nil {
104		return err
105	}
106	return merge.Maps(target, maps...)
107}
108
109func (m *Merger) mergeToMap(input interface{}, target map[string]interface{}) error {
110	if input == nil {
111		return nil
112	}
113	switch v := input.(type) {
114	case string:
115		// load by file extension
116		if ext := getExtension(v); ext != "" {
117			lext := strings.ToLower(ext)
118			if f, found := m.loadersByExt[lext]; found {
119				m, err := f.Load(v)
120				if err != nil {
121					return err
122				}
123				return merge.Maps(target, m...)
124			}
125		}
126		err := m.tryLoaders(v, target)
127		if err != nil {
128			return err
129		}
130	case io.Reader:
131		// read into []byte in case it's drained when try different load
132		bs, err := io.ReadAll(v)
133		if err != nil {
134			return err
135		}
136		err = m.tryLoaders(bs, target)
137		if err != nil {
138			return err
139		}
140	case []string:
141		for _, v := range v {
142			err := m.mergeToMap(v, target)
143			if err != nil {
144				return err
145			}
146		}
147	case []io.Reader:
148		for _, v := range v {
149			err := m.mergeToMap(v, target)
150			if err != nil {
151				return err
152			}
153		}
154	default:
155		return m.tryLoaders(v, target)
156	}
157	return nil
158}
159
160func (m *Merger) tryLoaders(input interface{}, target map[string]interface{}) error {
161	var errs []string
162	for _, f := range m.loadersByName {
163		m, err := f.Load(input)
164		if err == nil {
165			return merge.Maps(target, m...)
166		}
167		errs = append(errs, fmt.Sprintf("[%s] %s", f.Name, err))
168	}
169	return fmt.Errorf("tried all formats but failed: %s", strings.Join(errs, "; "))
170}