ohler55 / ojg

Optimized JSON for Go
MIT License
837 stars 50 forks source link

Support Keyed and Indexed interfaces in `jp.Expr.Modify` #156

Closed twelvelabs closed 2 months ago

twelvelabs commented 8 months ago

Hello!

First off, many thanks for a fantastic library! It's made my life a lot easier and I really appreciate it.

I'm currently using ojg/jp in a CLI tool that (among other things) allows users to modify JSON and YAML files via JSON path expressions. In both cases I'm unmarshalling the files to map[string]any and passing them into jp.Expr.Modify().

One thing I'd like to add is the ability to maintain comments, whitespace, and key ordering when updating YAML files. The YAML lib I'm using supports this when unmarshalling into a yaml.Node, so I've been looking into using that instead of an untyped map.

I noticed the recent addition of the jp.Keyed and jp.Indexed interfaces, and I was thinking about creating a wrapper around yaml.Node that implemented those interfaces. This seems like it would do what I want.

Unfortunately it doesn't look like the jp.Expr.Modify() method supports those new interfaces. That or perhaps I'm invoking it incorrectly 🤷. Here's what I've tried:

package main

import (
    "fmt"
    "os"
    "sort"

    "github.com/k0kubun/pp/v3"
    "github.com/ohler55/ojg/jp"
    "github.com/ohler55/ojg/oj"
)

var _ jp.Keyed = &Node{}

func NewNode(m map[string]any) *Node {
    return &Node{
        data: m,
    }
}

type Node struct {
    data map[string]any
}

func (n *Node) ValueForKey(key string) (any, bool) {
    if value, has := n.data[key]; has {
        return value, true
    }
    return nil, false
}
func (n *Node) SetValueForKey(key string, value any) {
    n.data[key] = value
}
func (n *Node) RemoveValueForKey(key string) {
    delete(n.data, key)
}
func (n *Node) Keys() []string {
    keys := []string{}
    for k := range n.data {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    return keys
}

func main() {
    data := NewNode(map[string]any{
        "a": NewNode(map[string]any{"x": 1, "y": 2, "z": 3}),
        "b": NewNode(map[string]any{"x": 4, "y": 5, "z": 6}),
    })

    fmt.Println("")
    exp := jp.MustParseString("$.*.z")
    fmt.Println("Expression: " + exp.String())

    found := exp.Get(data)
    fmt.Println("Get: " + oj.JSON(found))
    fmt.Println("")

    // fmt.Println("Set:")
    // _ = exp.Set(data, 999)
    // pp.Println(data)
    // fmt.Println("")

    fmt.Println("Modified:")
    modified := exp.MustModify(data, func(element any) (any, bool) {
        return 999, true
    })
    pp.Println(modified)
    fmt.Println("")

    os.Exit(0)
}

Which returns:


Expression: $.*.z
Get: [3,6]

Modified:
&main.Node{
  data: map[string]interface {}{
    "a": &main.Node{
      data: map[string]interface {}{
        "x": 1,
        "y": 2,
        "z": 3,
      },
    },
    "b": &main.Node{
      data: map[string]interface {}{
        "x": 4,
        "y": 5,
        "z": 6,
      },
    },
  },
}

I expected the z keys in the data maps to be set to 999. Works as expected when removing the Node wrappers. Also works when using jp.Expr.Set().

I didn't see any references to Indexed or Keyed in https://github.com/ohler55/ojg/blob/develop/jp/modify.go, so I'm thinking that Modify just does't currently support them. I would love to help out and add that myself, but honestly that code is a bit beyond me 😞. Is there any chance you'd be interested in doing so?

ohler55 commented 8 months ago

I'll have to look at that carefully. It might require another interface to support modifying a keyed or indexed.

ohler55 commented 2 months ago

My apologies that it has taken this long for me to get to this. I've started a branch called modify-keyed-indexed that has the start of the updates. All have been implemented but minimally tested if you want to try it out as I add the tests.

ohler55 commented 2 months ago

Added in release v1.22.1