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}