diff --git a/extracter/README.md b/extracter/README.md index 417dc65..29058ec 100644 --- a/extracter/README.md +++ b/extracter/README.md @@ -1,6 +1,6 @@ -# JSON Extracter +# Extracter -Extract specific field from JSON and **output not only the field value but also its upstream structure**. +Extract specific field from JSON-like data and **output not only the field value but also its upstream structure**. A typical use case is to trim k8s objects in `TransformingInformer` to save informer memory. @@ -17,7 +17,7 @@ import ( "encoding/json" "fmt" - "kusionstack.io/kube-utils/jsonextracter" + "kusionstack.io/kube-utils/extracter" ) var pod = []byte(`{ @@ -57,18 +57,19 @@ func main() { json.Unmarshal(pod, &podData) kindPath := "{.kind}" - kindExtracter, _ := jsonextracter.BuildExtracter(kindPath, false) + kindExtracter, _ := extracter.New([]string{kindPath}, false) kind, _ := kindExtracter.Extract(podData) printJSON(kind) nameImagePath := "{.spec.containers[*]['name', 'image']}" - nameImageExtracter, _ := jsonextracter.BuildExtracter(nameImagePath, false) + nameImageExtracter, _ := extracter.New([]string{nameImagePath}, false) nameImage, _ := nameImageExtracter.Extract(podData) printJSON(nameImage) - merged, _ := jsonextracter.Merge([]jsonextracter.Extracter{kindExtracter, nameImageExtracter}, podData) + mergeExtracter, _ := extracter.New([]string{kindPath, nameImagePath}, false) + merged, _ := mergeExtracter.Extract(podData) printJSON(merged) } ``` @@ -83,19 +84,17 @@ Output: ## Note -The merge behavior of the `jsonextracter.Merge` on the list is replacing. Therefore, if you retrieve the container name and image separately and merge them, the resulting output will not contain the image. +The merge behavior on the list is replacing. Therefore, if you retrieve the container name and image separately and merge them, the resulting output will not contain the image. Code: ```go ... namePath := "{.spec.containers[*].name}" - nameExtracter, _ := jsonextracter.BuildExtracter(namePath, false) - imagePath := "{.spec.containers[*].image}" - imageExtracter, _ := jsonextracter.BuildExtracter(imagePath, false) - merged, _ = jsonextracter.Merge([]jsonextracter.Extracter{imageExtracter, nameExtracter}, podData) + mergeExtracter, _ = extracter.New([]string{imagePath, namePath}, false) + merged, _ = mergeExtracter.Extract(podData) printJSON(merged) ... ``` diff --git a/extracter/alias.go b/extracter/alias.go index 96e3df2..f5fafc6 100644 --- a/extracter/alias.go +++ b/extracter/alias.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "k8s.io/client-go/util/jsonpath" @@ -36,5 +36,3 @@ type ( UnionNode = jsonpath.UnionNode IdentifierNode = jsonpath.IdentifierNode ) - -var Parse = jsonpath.Parse diff --git a/extracter/extracter.go b/extracter/extracter.go index ba5b480..7439629 100644 --- a/extracter/extracter.go +++ b/extracter/extracter.go @@ -14,9 +14,10 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( + "errors" "fmt" "k8s.io/client-go/util/jsonpath" @@ -26,34 +27,87 @@ type Extracter interface { Extract(data map[string]interface{}) (map[string]interface{}, error) } -// BuildExtracter automatically determines whether to use FieldPathExtracter or JSONPathExtracter. -// If the input jsonPath only involves map operations, it will return FieldPathExtracter, -// as it has better performance. -func BuildExtracter(jsonPath string, allowMissingKeys bool) (Extracter, error) { - parser, err := Parse(jsonPath, jsonPath) +// parse is unlike the jsonpath.Parse, which supports multi-paths input. +// The input like `{.kind} {.apiVersion}` or +// `{range .spec.containers[*]}{.name}{end}` will result in an error. +func parse(name, text string) (*Parser, error) { + p, err := jsonpath.Parse(name, text) if err != nil { - return nil, fmt.Errorf("error in parsing path %q: %w", jsonPath, err) + return nil, err } - rootNodes := parser.Root.Nodes - if len(rootNodes) == 0 { - return NewNestedFieldPath(nil, allowMissingKeys), nil + if len(p.Root.Nodes) > 1 { + return nil, errors.New("not support multi-paths input") } - if len(rootNodes) == 1 { - nodes := rootNodes[0].(*jsonpath.ListNode).Nodes - fields := make([]string, 0, len(nodes)) - for _, node := range nodes { - if node.Type() == jsonpath.NodeField { - fields = append(fields, node.(*jsonpath.FieldNode).Value) + return p, nil +} + +// New creates an Extracter. For each jsonPaths, FieldPathExtracter will +// be parsed whenever possible, as it has better performance +func New(jsonPaths []string, allowMissingKeys bool) (Extracter, error) { + var extracters []Extracter + + for _, p := range jsonPaths { + parser, err := parse(p, p) + if err != nil { + return nil, fmt.Errorf("error in parsing path %q: %w", p, err) + } + + rootNodes := parser.Root.Nodes + if len(rootNodes) == 0 { + extracters = append(extracters, NewNestedFieldPathExtracter(nil, allowMissingKeys)) + continue + } + + if len(rootNodes) == 1 { + nodes := rootNodes[0].(*jsonpath.ListNode).Nodes + fields := make([]string, 0, len(nodes)) + for _, node := range nodes { + if node.Type() == jsonpath.NodeField { + fields = append(fields, node.(*jsonpath.FieldNode).Value) + } } + + if len(nodes) == len(fields) { + fp := NewNestedFieldPathExtracter(fields, allowMissingKeys) + extracters = append(extracters, fp) + continue + } + } + + jp := &JSONPathExtracter{name: parser.Name, parser: parser, allowMissingKeys: allowMissingKeys} + extracters = append(extracters, jp) + } + + if len(extracters) == 1 { + return extracters[0], nil + } + + return &Extracters{extracters}, nil +} + +// Extracters makes it easy when you want to extract multi fields and merge them. +type Extracters struct { + extracters []Extracter +} + +// Extract calls all extracters in order and merges their outputs by calling MergeFields. +func (e *Extracters) Extract(data map[string]interface{}) (map[string]interface{}, error) { + var merged map[string]interface{} + + for _, ex := range e.extracters { + field, err := ex.Extract(data) + if err != nil { + return nil, err } - if len(nodes) == len(fields) { - return NewNestedFieldPath(fields, allowMissingKeys), nil + if merged == nil { + merged = field + } else { + merged = MergeFields(merged, field) } } - jp := &JSONPath{name: parser.Name, parser: parser, allowMissingKeys: allowMissingKeys} - return jp, nil + return merged, nil } diff --git a/extracter/extracter_test.go b/extracter/extracter_test.go index 70d9d21..c12a525 100644 --- a/extracter/extracter_test.go +++ b/extracter/extracter_test.go @@ -14,16 +14,17 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( + "encoding/json" "reflect" "testing" ) -func TestBuildExtracter(t *testing.T) { +func TestNew(t *testing.T) { type args struct { - path string + paths []string allowMissingKeys bool } tests := []struct { @@ -32,25 +33,72 @@ func TestBuildExtracter(t *testing.T) { want Extracter wantErr bool }{ - {name: "invalid path", args: args{path: `{`, allowMissingKeys: false}, want: nil, wantErr: true}, - {name: "fieldPath extracter", args: args{path: `{}`, allowMissingKeys: false}, want: &NestedFieldPath{}, wantErr: false}, - {name: "fieldPath extracter", args: args{path: ``, allowMissingKeys: false}, want: &NestedFieldPath{}, wantErr: false}, - {name: "fieldPath extracter", args: args{path: `{.metadata.labels.name}`, allowMissingKeys: false}, want: &NestedFieldPath{}, wantErr: false}, - {name: "fieldPath extracter", args: args{path: `{.metadata.labels['name']}`, allowMissingKeys: false}, want: &NestedFieldPath{}, wantErr: false}, - {name: "jsonPath extracter", args: args{path: `{.metadata.labels.name}{.metadata.labels.app}`, allowMissingKeys: false}, want: &JSONPath{}, wantErr: false}, - {name: "jsonPath extracter", args: args{path: `{.metadata.labels['name', 'app']}`, allowMissingKeys: false}, want: &JSONPath{}, wantErr: false}, - {name: "jsonPath extracter", args: args{path: `{.spec.containers[*].name}`, allowMissingKeys: false}, want: &JSONPath{}, wantErr: false}, + {name: "invalid path", args: args{paths: []string{`{`}, allowMissingKeys: false}, want: nil, wantErr: true}, + {name: "fieldPath extracter", args: args{paths: []string{`{}`}, allowMissingKeys: false}, want: &NestedFieldPathExtracter{}, wantErr: false}, + {name: "fieldPath extracter", args: args{paths: []string{``}, allowMissingKeys: false}, want: &NestedFieldPathExtracter{}, wantErr: false}, + {name: "fieldPath extracter", args: args{paths: []string{`{.metadata.labels.name}`}, allowMissingKeys: false}, want: &NestedFieldPathExtracter{}, wantErr: false}, + {name: "fieldPath extracter", args: args{paths: []string{`{.metadata.labels['name']}`}, allowMissingKeys: false}, want: &NestedFieldPathExtracter{}, wantErr: false}, + {name: "jsonPath extracter", args: args{paths: []string{`{.metadata.labels.name}{.metadata.labels.app}`}, allowMissingKeys: false}, want: nil, wantErr: true}, + {name: "jsonPath extracter", args: args{paths: []string{`{.metadata.labels['name', 'app']}`}, allowMissingKeys: false}, want: &JSONPathExtracter{}, wantErr: false}, + {name: "jsonPath extracter", args: args{paths: []string{`{.spec.containers[*].name}`}, allowMissingKeys: false}, want: &JSONPathExtracter{}, wantErr: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := BuildExtracter(tt.args.path, tt.args.allowMissingKeys) + got, err := New(tt.args.paths, tt.args.allowMissingKeys) if (err != nil) != tt.wantErr { - t.Errorf("BuildExtracter() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return } if reflect.TypeOf(tt.want) != reflect.TypeOf(got) { - t.Errorf("BuildExtracter() = %T, want %T", got, tt.want) + t.Errorf("New() = %T, want %T", got, tt.want) + } + }) + } +} + +func TestExtracters_Extract(t *testing.T) { + containerNamePath := `{.spec.containers[*].name}` + containerImagePath := `{.spec.containers[*].image}` + kindPath := "{.kind}" + apiVersionPath := "{.apiVersion}" + + type args struct { + paths []string + input map[string]interface{} + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "merge name and image", args: args{paths: []string{containerImagePath, containerNamePath}, input: podData}, + want: `{"spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, wantErr: false, + }, + { + name: "name kind apiVersion", args: args{paths: []string{containerNamePath, kindPath, apiVersionPath}, input: podData}, + want: `{"apiVersion":"v1","kind":"Pod","spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ex, err := New(tt.args.paths, true) + if (err != nil) != tt.wantErr { + t.Errorf("Extracters_Extract() error = %v, wantErr %v", err, tt.wantErr) + return + } + + got, err := ex.Extract(tt.args.input) + if (err != nil) != tt.wantErr { + t.Errorf("Extracters_Extract() error = %v, wantErr %v", err, tt.wantErr) + return + } + + data, _ := json.Marshal(got) + if string(data) != tt.want { + t.Errorf("Extracters_Extract() = %v, want %v", string(data), tt.want) } }) } diff --git a/extracter/fieldpath.go b/extracter/fieldpath.go index 3bea37b..675e5c8 100644 --- a/extracter/fieldpath.go +++ b/extracter/fieldpath.go @@ -14,26 +14,26 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "fmt" ) -// NewNestedFieldPath constructs a FieldPathExtracter. -func NewNestedFieldPath(nestedField []string, allowMissingKeys bool) *NestedFieldPath { - return &NestedFieldPath{nestedField: nestedField, allowMissingKeys: allowMissingKeys} +// NewNestedFieldPathExtracter constructs a FieldPathExtracter. +func NewNestedFieldPathExtracter(nestedField []string, allowMissingKeys bool) *NestedFieldPathExtracter { + return &NestedFieldPathExtracter{nestedField: nestedField, allowMissingKeys: allowMissingKeys} } -// NestedFieldPath is used to wrap NestedFieldNoCopy function as an Extracter. -type NestedFieldPath struct { +// NestedFieldPathExtracter is used to wrap NestedFieldNoCopy function as an Extracter. +type NestedFieldPathExtracter struct { nestedField []string allowMissingKeys bool } // Extract outputs the nestedField's value and its upstream structure. -func (f *NestedFieldPath) Extract(data map[string]interface{}) (map[string]interface{}, error) { - return NestedFieldNoCopy(data, f.allowMissingKeys, f.nestedField...) +func (n *NestedFieldPathExtracter) Extract(data map[string]interface{}) (map[string]interface{}, error) { + return NestedFieldNoCopy(data, n.allowMissingKeys, n.nestedField...) } // NestedFieldNoCopy is similar to JSONPath.Extract. The difference is that it diff --git a/extracter/fieldpath_test.go b/extracter/fieldpath_test.go index 9e03b7c..78a607b 100644 --- a/extracter/fieldpath_test.go +++ b/extracter/fieldpath_test.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "encoding/json" diff --git a/extracter/jsonpath.go b/extracter/jsonpath.go index 4c4620e..329cf2c 100644 --- a/extracter/jsonpath.go +++ b/extracter/jsonpath.go @@ -17,7 +17,7 @@ // Copied and adapted from https://github.com/kubernetes/client-go/blob/master/util/jsonpath/jsonpath.go -package jsonextracter +package extracter import ( "fmt" @@ -27,7 +27,7 @@ import ( "k8s.io/client-go/third_party/forked/golang/template" ) -type JSONPath struct { +type JSONPathExtracter struct { name string parser *Parser beginRange int @@ -39,9 +39,9 @@ type JSONPath struct { allowMissingKeys bool } -// New creates a new JSONPath with the given name. -func New(name string) *JSONPath { - return &JSONPath{ +// NewJSONPathExtracter creates a new JSONPath with the given name. +func NewJSONPathExtracter(name string) *JSONPathExtracter { + return &JSONPathExtracter{ name: name, beginRange: 0, inRange: 0, @@ -51,15 +51,15 @@ func New(name string) *JSONPath { // AllowMissingKeys allows a caller to specify whether they want an error if a field or map key // cannot be located, or simply an empty result. The receiver is returned for chaining. -func (j *JSONPath) AllowMissingKeys(allow bool) *JSONPath { +func (j *JSONPathExtracter) AllowMissingKeys(allow bool) *JSONPathExtracter { j.allowMissingKeys = allow return j } // Parse parses the given template and returns an error. -func (j *JSONPath) Parse(text string) error { +func (j *JSONPathExtracter) Parse(text string) error { var err error - j.parser, err = Parse(j.name, text) + j.parser, err = parse(j.name, text) return err } @@ -80,7 +80,7 @@ func makeNopSetFieldFuncSlice(n int) []setFieldFunc { // // The data structure of the extracted field must be of type `map[string]interface{}`, // and `struct` is not supported (an error will be returned). -func (j *JSONPath) Extract(data map[string]interface{}) (map[string]interface{}, error) { +func (j *JSONPathExtracter) Extract(data map[string]interface{}) (map[string]interface{}, error) { container := struct{ Root reflect.Value }{} setFn := func(val reflect.Value) error { container.Root = val @@ -99,7 +99,7 @@ func (j *JSONPath) Extract(data map[string]interface{}) (map[string]interface{}, return container.Root.Interface().(map[string]interface{}), nil } -func (j *JSONPath) FindResults(data interface{}, setFn setFieldFunc) ([][]reflect.Value, error) { +func (j *JSONPathExtracter) FindResults(data interface{}, setFn setFieldFunc) ([][]reflect.Value, error) { if j.parser == nil { return nil, fmt.Errorf("%s is an incomplete jsonpath template", j.name) } @@ -159,7 +159,7 @@ func (j *JSONPath) FindResults(data interface{}, setFn setFieldFunc) ([][]reflec return fullResult, nil } -func (j *JSONPath) _walk(value []reflect.Value, node Node, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) _walk(value []reflect.Value, node Node, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { switch node := node.(type) { case *ListNode: return j._evalList(value, node, setFn) @@ -179,7 +179,7 @@ func (j *JSONPath) _walk(value []reflect.Value, node Node, setFn []setFieldFunc) } // walk visits tree rooted at the given node in DFS order -func (j *JSONPath) walk(value []reflect.Value, node Node) ([]reflect.Value, error) { +func (j *JSONPathExtracter) walk(value []reflect.Value, node Node) ([]reflect.Value, error) { switch node := node.(type) { case *ListNode: return j.evalList(value, node) @@ -215,7 +215,7 @@ func (j *JSONPath) walk(value []reflect.Value, node Node) ([]reflect.Value, erro } // evalInt evaluates IntNode -func (j *JSONPath) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Value, error) { result := make([]reflect.Value, len(input)) for i := range input { result[i] = reflect.ValueOf(node.Value) @@ -224,7 +224,7 @@ func (j *JSONPath) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Valu } // evalFloat evaluates FloatNode -func (j *JSONPath) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect.Value, error) { result := make([]reflect.Value, len(input)) for i := range input { result[i] = reflect.ValueOf(node.Value) @@ -233,7 +233,7 @@ func (j *JSONPath) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect. } // evalBool evaluates BoolNode -func (j *JSONPath) evalBool(input []reflect.Value, node *BoolNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalBool(input []reflect.Value, node *BoolNode) ([]reflect.Value, error) { result := make([]reflect.Value, len(input)) for i := range input { result[i] = reflect.ValueOf(node.Value) @@ -241,7 +241,7 @@ func (j *JSONPath) evalBool(input []reflect.Value, node *BoolNode) ([]reflect.Va return result, nil } -func (j *JSONPath) _evalList(value []reflect.Value, node *ListNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) _evalList(value []reflect.Value, node *ListNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { var err error curValue := value curFns := setFn @@ -256,7 +256,7 @@ func (j *JSONPath) _evalList(value []reflect.Value, node *ListNode, setFn []setF } // evalList evaluates ListNode -func (j *JSONPath) evalList(value []reflect.Value, node *ListNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalList(value []reflect.Value, node *ListNode) ([]reflect.Value, error) { var err error curValue := value for _, node := range node.Nodes { @@ -269,7 +269,7 @@ func (j *JSONPath) evalList(value []reflect.Value, node *ListNode) ([]reflect.Va } // evalIdentifier evaluates IdentifierNode -func (j *JSONPath) evalIdentifier(input []reflect.Value, node *IdentifierNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) evalIdentifier(input []reflect.Value, node *IdentifierNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { results := []reflect.Value{} switch node.Name { case "range": @@ -288,7 +288,7 @@ func (j *JSONPath) evalIdentifier(input []reflect.Value, node *IdentifierNode, s } // evalArray evaluates ArrayNode -func (j *JSONPath) evalArray(input []reflect.Value, node *ArrayNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) evalArray(input []reflect.Value, node *ArrayNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { result := []reflect.Value{} nextFns := []setFieldFunc{} for k, value := range input { @@ -360,7 +360,7 @@ func (j *JSONPath) evalArray(input []reflect.Value, node *ArrayNode, setFn []set return result, nextFns, nil } -func (j *JSONPath) _evalUnion(input []reflect.Value, node *UnionNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) _evalUnion(input []reflect.Value, node *UnionNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { result := []reflect.Value{} fns := []setFieldFunc{} @@ -403,7 +403,7 @@ func (j *JSONPath) _evalUnion(input []reflect.Value, node *UnionNode, setFn []se } // evalUnion evaluates UnionNode -func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.Value, error) { result := []reflect.Value{} for _, listNode := range node.Nodes { temp, err := j.evalList(input, listNode) @@ -416,7 +416,7 @@ func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect. } //lint:ignore U1000 ignore unused function -func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) { +func (j *JSONPathExtracter) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) { t := value.Type() var inlineValue *reflect.Value for ix := 0; ix < t.NumField(); ix++ { @@ -450,7 +450,7 @@ func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (refl } // evalField evaluates field of struct or key of map. -func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) evalField(input []reflect.Value, node *FieldNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { results := []reflect.Value{} nextFns := []setFieldFunc{} // If there's no input, there's no output @@ -502,7 +502,7 @@ func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode, setFn []set } // evalWildcard extracts all contents of the given value -func (j *JSONPath) evalWildcard(input []reflect.Value, _ *WildcardNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalWildcard(input []reflect.Value, _ *WildcardNode) ([]reflect.Value, error) { results := []reflect.Value{} for _, value := range input { value, isNil := template.Indirect(value) @@ -529,7 +529,7 @@ func (j *JSONPath) evalWildcard(input []reflect.Value, _ *WildcardNode) ([]refle } // evalRecursive visits the given value recursively and pushes all of them to result -func (j *JSONPath) evalRecursive(input []reflect.Value, node *RecursiveNode) ([]reflect.Value, error) { +func (j *JSONPathExtracter) evalRecursive(input []reflect.Value, node *RecursiveNode) ([]reflect.Value, error) { result := []reflect.Value{} for _, value := range input { results := []reflect.Value{} @@ -565,7 +565,7 @@ func (j *JSONPath) evalRecursive(input []reflect.Value, node *RecursiveNode) ([] } // evalFilter filters array according to FilterNode -func (j *JSONPath) evalFilter(input []reflect.Value, node *FilterNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { +func (j *JSONPathExtracter) evalFilter(input []reflect.Value, node *FilterNode, setFn []setFieldFunc) ([]reflect.Value, []setFieldFunc, error) { results := []reflect.Value{} fns := []setFieldFunc{} for k, value := range input { diff --git a/extracter/jsonpath_test.go b/extracter/jsonpath_test.go index 44efd97..0aca18b 100644 --- a/extracter/jsonpath_test.go +++ b/extracter/jsonpath_test.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "encoding/json" @@ -29,8 +29,8 @@ type jsonPathTest struct { expectError bool } -func (t *jsonPathTest) Prepare(allowMissingKeys bool) (*JSONPath, error) { - jp := New(t.name) +func (t *jsonPathTest) Prepare(allowMissingKeys bool) (*JSONPathExtracter, error) { + jp := NewJSONPathExtracter(t.name) jp.AllowMissingKeys(allowMissingKeys) return jp, jp.Parse(t.template) } @@ -146,7 +146,7 @@ func TestJSONPath(t *testing.T) { {"empty", ``, podData, `null`, false}, {"containers name", `{.kind}`, podData, `{"kind":"Pod"}`, false}, {"containers name", `{.spec.containers[*].name}`, podData, `{"spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, false}, - {"containers name (range)", `{range .spec.containers[*]}{.name}{end}`, podData, `{"spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, false}, + {"containers name (range)", `{range .spec.containers[*]}{.name}{end}`, podData, `null`, true}, {"containers name and image", `{.spec.containers[*]['name', 'image']}`, podData, `{"spec":{"containers":[{"image":"registry.k8s.io/pause:3.8","name":"pause1"},{"image":"registry.k8s.io/pause:3.8","name":"pause2"}]}}`, false}, {"containers name and cpu", `{.spec.containers[*]['name', 'resources.requests.cpu']}`, podData, `{"spec":{"containers":[{"name":"pause1","resources":{"requests":{"cpu":"100m"}}},{"name":"pause2","resources":{"requests":{"cpu":"10m"}}}]}}`, false}, {"container pause1 name and image", `{.spec.containers[?(@.name=="pause1")]['name', 'image']}`, podData, `{"spec":{"containers":[{"image":"registry.k8s.io/pause:3.8","name":"pause1"}]}}`, false}, diff --git a/extracter/merge.go b/extracter/merge.go index 1e66cf5..f9ebf0e 100644 --- a/extracter/merge.go +++ b/extracter/merge.go @@ -14,33 +14,12 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "reflect" ) -// Merge is a helper function that calls all extracters and merges their -// outputs by calling MergeFields. -func Merge(extracters []Extracter, input map[string]interface{}) (map[string]interface{}, error) { - var merged map[string]interface{} - - for _, ex := range extracters { - field, err := ex.Extract(input) - if err != nil { - return nil, err - } - - if merged == nil { - merged = field - } else { - merged = MergeFields(merged, field) - } - } - - return merged, nil -} - // MergeFields merges src into dst. // // Note: the merge operation on two nested list is replacing. diff --git a/extracter/merge_test.go b/extracter/merge_test.go index ada0677..168d67b 100644 --- a/extracter/merge_test.go +++ b/extracter/merge_test.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package jsonextracter +package extracter import ( "bytes" @@ -34,7 +34,6 @@ func BenchmarkJSONPathMerge(b *testing.B) { } extracters := make([]Extracter, 0) - for _, test := range tests { ex, err := test.Prepare(false) if err != nil { @@ -46,10 +45,11 @@ func BenchmarkJSONPathMerge(b *testing.B) { extracters = append(extracters, ex) } + ex := Extracters{extracters: extracters} b.ResetTimer() for n := 0; n < b.N; n++ { - Merge(extracters, podData) + ex.Extract(podData) } } @@ -57,15 +57,15 @@ func BenchmarkFieldPathMerge(b *testing.B) { fields := []string{"kind", "apiVersion", "metadata"} extracters := make([]Extracter, 0) - for _, f := range fields { - extracters = append(extracters, NewNestedFieldPath([]string{f}, false)) + extracters = append(extracters, NewNestedFieldPathExtracter([]string{f}, false)) } + ex := Extracters{extracters: extracters} b.ResetTimer() for n := 0; n < b.N; n++ { - Merge(extracters, podData) + ex.Extract(podData) } } @@ -85,49 +85,3 @@ func BenchmarkTmpl(b *testing.B) { json.Unmarshal(buf.Bytes(), &dest) } } - -func TestMerge(t *testing.T) { - containerName := jsonPathTest{"containers name", `{.spec.containers[*].name}`, podData, "", false} - containerNameExtracter, _ := containerName.Prepare(true) - - containerImage := jsonPathTest{"containers image", `{.spec.containers[*].image}`, podData, "", false} - containerImageExtracter, _ := containerImage.Prepare(true) - - kindExtracter := NewNestedFieldPath([]string{"kind"}, true) - - apiVersionExtracter := NewNestedFieldPath([]string{"apiVersion"}, true) - - type args struct { - extracters []Extracter - input map[string]interface{} - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "merge name and image", args: args{extracters: []Extracter{containerImageExtracter, containerNameExtracter}, input: podData}, - want: `{"spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, wantErr: false, - }, - { - name: "name kind apiVersion", args: args{extracters: []Extracter{containerNameExtracter, kindExtracter, apiVersionExtracter}, input: podData}, - want: `{"apiVersion":"v1","kind":"Pod","spec":{"containers":[{"name":"pause1"},{"name":"pause2"}]}}`, wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := Merge(tt.args.extracters, tt.args.input) - if (err != nil) != tt.wantErr { - t.Errorf("Merge() error = %v, wantErr %v", err, tt.wantErr) - return - } - - data, _ := json.Marshal(got) - if string(data) != tt.want { - t.Errorf("Merge() = %v, want %v", string(data), tt.want) - } - }) - } -}