readme.md

  1# go-jsons
  2
  3A universal `JSON` merge library for `Go`. 
  4
  5(test coverage: 100.0%)
  6
  7## Installation
  8
  9```bash
 10go get github.com/qjebbs/go-jsons
 11```
 12
 13## Usage
 14
 15```go
 16a := []byte(`{"a":1}`)
 17b := []byte(`{"b":[1]}`)
 18c := []byte(`{"b":[2]}`)
 19got, err := jsons.Merge(a, b, c) // got = []byte(`{"a":1,"b":[1,2]}`)
 20```
 21
 22### Accepted input
 23
 24- `string`: path to a local file
 25- `[]string`: paths of local files
 26- `[]byte`: content of a file
 27- `[][]byte`: content list of files
 28- `io.Reader`: content reader
 29- `[]io.Reader`: content readers
 30
 31## Merge rules
 32
 33The strandard merger has only one rule which is intuitive and easy to understand:
 34
 35- Simple values (`string`, `number`, `boolean`) are overwritten, others (`array`, `object`) are merged.
 36
 37To work with complex files and contents, you can create a custom merger to applies more rules:
 38
 39```go
 40var myMerger = NewMerger(
 41	rule.MergeBy("tag"),
 42	rule.MergeByAndRemove("_tag"),
 43	rule.OrderByAndRemove("_order"),
 44)
 45myMerger.Merge("a.json", "b.json")
 46```
 47
 48which means:
 49
 50- Elements with same `tag` or `_tag` in an array will be merged.
 51- Elements in an array will be sorted by the value of `_order` field, the smaller ones are in front.
 52
 53> `_tag` and `_order` fields will be removed after merge, according to the codes above.
 54
 55Suppose we have...
 56
 57`a.json`:
 58
 59```json
 60{
 61  "log": {"level": "debug"},
 62  "inbounds": [{"tag": "in-1"}],
 63  "outbounds": [{"_order": 100, "tag": "out-1"}],
 64  "route": {"rules": [
 65    {"_tag":"rule1","inbound":["in-1"],"outbound":"out-1"}
 66  ]}
 67}
 68```
 69
 70`b.json`:
 71
 72```json
 73{
 74  "log": {"level": "error"},
 75  "outbounds": [{"_order": -100, "tag": "out-2"}],
 76  "route": {"rules": [
 77    {"_tag":"rule1","inbound":["in-1.1"],"outbound":"out-1.1"}
 78  ]}
 79}
 80```
 81
 82Output:
 83
 84```jsonc
 85{
 86  // level field is overwritten by the latter value
 87  "log": {"level": "error"},
 88  "inbounds": [{"tag": "in-1"}],
 89  "outbounds": [
 90    // Although out-2 is a latecomer, but it's in 
 91    // the front due to the smaller "_order"
 92    {"tag": "out-2"},
 93    {"tag": "out-1"}
 94  ],
 95  "route": {"rules": [
 96    // 2 rules are merged into one due to the same "_tag",
 97    // outbound field is overwritten during the merging
 98    {"inbound":["in-1","in-1.1"],"outbound":"out-1.1"}
 99  ]}
100}
101```
102
103## Load from other formats
104
105`go-jsons` allows you to extend it to load other formats easily.
106
107For example, to load from `YAML` files and merge to `JSON`:
108
109```go
110package main
111
112import (
113	"fmt"
114
115	"github.com/qjebbs/go-jsons"
116	// yaml v3 is required, since v2 generates `map[interface{}]interface{}`,
117	// which is not compatible with json.Marshal
118	"gopkg.in/yaml.v3"
119)
120
121func main() {
122	const FormatYAML jsons.Format = "yaml"
123	m := jsons.NewMerger()
124	m.RegisterLoader(
125		FormatYAML,
126		[]string{".yaml", ".yml"},
127		func(b []byte) (map[string]interface{}, error) {
128			m := make(map[string]interface{})
129			err := yaml.Unmarshal(b, &m)
130			if err != nil {
131				return nil, err
132			}
133			return m, nil
134		},
135	)
136	a := []byte(`{"a": 1}`) // json
137	b := []byte(`b: 1`)     // yaml
138	got, err := m.Merge(a, b)
139	if err != nil {
140		panic(err)
141	}
142	fmt.Println(string(got)) // {"a":1,"b":1}
143}
144```
145
146## Why not support remote files?
147
148Here are my considerations:
149
150- It makes the your program support remote file unexpectedly, which may be a security risk.
151- Users need to choose their own strategy for loading remote files, not hard-coded logic in the library
152- You can still merge downloaded content by `[]byte` or `io.Reader`