dyno

package module
v0.0.0-...-09f820a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2023 License: Apache-2.0 Imports: 1 Imported by: 87

README

dyno

Build Status Go Reference Go Report Card codecov

Package dyno is a utility to work with dynamic objects at ease.

Primary goal is to easily handle dynamic objects and arrays (and a mixture of these) that are the result of unmarshaling a JSON or YAML text into an interface{} for example. When unmarshaling into interface{}, libraries usually choose either map[string]interface{} or map[interface{}]interface{} to represent objects, and []interface{} to represent arrays. Package dyno supports a mixture of these in any depth and combination.

When operating on a dynamic object, you designate a value you're interested in by specifying a path. A path is a navigation; it is a series of map keys and int slice indices that tells how to get to the value.

Should you need to marshal a dynamic object to JSON which contains maps with interface{} key type (which is not supported by encoding/json), you may use the ConvertMapI2MapS converter function.

The implementation does not use reflection at all, so performance is rather good.

Supported Operations
Example

Let's see a simple example editing a JSON text to mask out a password. This is a simplified version of the Example_jsonEdit example function:

src := `{"login":{"password":"secret","user":"bob"},"name":"cmpA"}`
var v interface{}
if err := json.Unmarshal([]byte(src), &v); err != nil {
	panic(err)
}
// Edit (mask out) password:
if err = dyno.Set(v, "xxx", "login", "password"); err != nil {
	fmt.Printf("Failed to set password: %v\n", err)
}
edited, err := json.Marshal(v)
fmt.Printf("Edited JSON: %s, error: %v\n", edited, err)

Output will be:

Edited JSON: {"login":{"password":"xxx","user":"bob"},"name":"cmpA"}, error: <nil>

Documentation

Overview

Package dyno is a utility to work with dynamic objects at ease.

Primary goal is to easily handle dynamic objects and arrays (and a mixture of these) that are the result of unmarshaling a JSON or YAML text into an interface{} for example. When unmarshaling into interface{}, libraries usually choose either map[string]interface{} or map[interface{}]interface{} to represent objects, and []interface{} to represent arrays. Package dyno supports a mixture of these in any depth and combination.

When operating on a dynamic object, you designate a value you're interested in by specifying a path. A path is a navigation; it is a series of map keys and int slice indices that tells how to get to the value.

Should you need to marshal a dynamic object to JSON which contains maps with interface{} key type (which is not supported by encoding/json), you may use the ConvertMapI2MapS converter function.

The implementation does not use reflection at all, so performance is rather good.

Let's see a simple example editing a JSON text to mask out a password. This is a simplified version of the Example_jsonEdit example function:

src := `{"login":{"password":"secret","user":"bob"},"name":"cmpA"}`
var v interface{}
if err := json.Unmarshal([]byte(src), &v); err != nil {
	panic(err)
}
// Edit (mask out) password:
if err = dyno.Set(v, "xxx", "login", "password"); err != nil {
	fmt.Printf("Failed to set password: %v\n", err)
}
edited, err := json.Marshal(v)
fmt.Printf("Edited JSON: %s, error: %v\n", edited, err)

Output will be:

Edited JSON: {"login":{"password":"xxx","user":"bob"},"name":"cmpA"}, error: <nil>
Example

Example shows a few of dyno's features, such as getting, setting and appending values to / from a dynamic object.

package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/icza/dyno"
)

func main() {
	person := map[string]interface{}{
		"name": map[string]interface{}{
			"first": "Bob",
			"last":  "Archer",
		},
		"age": 22,
		"fruits": []interface{}{
			"apple", "banana",
		},
	}

	// pp prints the person
	pp := func(err error) {
		json.NewEncoder(os.Stdout).Encode(person) // Output JSON
		if err != nil {
			fmt.Println("ERROR:", err)
		}
	}

	// Print initial person and its first name:
	pp(nil)
	v, err := dyno.Get(person, "name", "first")
	fmt.Printf("First name: %v, error: %v\n", v, err)

	// Change first name:
	pp(dyno.Set(person, "Alice", "name", "first"))

	// Change complete name from map to a single string:
	pp(dyno.Set(person, "Alice Archer", "name"))

	// Print and increment age:
	age, err := dyno.GetInt(person, "age")
	fmt.Printf("Age: %v, error: %v\n", age, err)
	pp(dyno.Set(person, age+1, "age"))

	// Change a fruits slice element:
	pp(dyno.Set(person, "lemon", "fruits", 1))

	// Add a new fruit:
	pp(dyno.Append(person, "melon", "fruits"))

}
Output:

{"age":22,"fruits":["apple","banana"],"name":{"first":"Bob","last":"Archer"}}
First name: Bob, error: <nil>
{"age":22,"fruits":["apple","banana"],"name":{"first":"Alice","last":"Archer"}}
{"age":22,"fruits":["apple","banana"],"name":"Alice Archer"}
Age: 22, error: <nil>
{"age":23,"fruits":["apple","banana"],"name":"Alice Archer"}
{"age":23,"fruits":["apple","lemon"],"name":"Alice Archer"}
{"age":23,"fruits":["apple","lemon","melon"],"name":"Alice Archer"}
Example (JsonEdit)

Example_jsonEdit shows a simple example how JSON can be edited. The password placed in the JSON is masked out.

package main

import (
	"encoding/json"
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	src := `{"login":{"password":"secret","user":"bob"},"name":"cmpA"}`
	fmt.Printf("Input JSON:  %s\n", src)

	var v interface{}
	if err := json.Unmarshal([]byte(src), &v); err != nil {
		panic(err)
	}

	user, err := dyno.Get(v, "login", "user")
	fmt.Printf("User:        %-6s, error: %v\n", user, err)

	password, err := dyno.Get(v, "login", "password")
	fmt.Printf("Password:    %-6s, error: %v\n", password, err)

	// Edit (mask out) password:
	if err = dyno.Set(v, "xxx", "login", "password"); err != nil {
		fmt.Printf("Failed to set password: %v\n", err)
	}

	edited, err := json.Marshal(v)
	fmt.Printf("Edited JSON: %s, error: %v\n", edited, err)

}
Output:

Input JSON:  {"login":{"password":"secret","user":"bob"},"name":"cmpA"}
User:        bob   , error: <nil>
Password:    secret, error: <nil>
Edited JSON: {"login":{"password":"xxx","user":"bob"},"name":"cmpA"}, error: <nil>

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append(v interface{}, value interface{}, path ...interface{}) error

Append appends a value to a slice denoted by the path.

The slice denoted by path must already exist.

Path cannot be empty or nil, else an error is returned.

Example
package main

import (
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	m := map[string]interface{}{
		"a": []interface{}{
			"3", 2, []interface{}{1, "two", 3.3},
		},
	}

	printMap := func(err error) {
		fmt.Println(m)
		if err != nil {
			fmt.Println("ERROR:", err)
		}
	}

	printMap(dyno.Append(m, 4, "a"))
	printMap(dyno.Append(m, 9, "a", 2))
	printMap(dyno.Append(m, 1, "x"))

}
Output:

map[a:[3 2 [1 two 3.3] 4]]
map[a:[3 2 [1 two 3.3 9] 4]]
map[a:[3 2 [1 two 3.3 9] 4]]
ERROR: missing key: x (path element idx: 0)

func AppendMore

func AppendMore(v interface{}, values []interface{}, path ...interface{}) error

AppendMore appends values to a slice denoted by the path.

The slice denoted by path must already exist.

Path cannot be empty or nil, else an error is returned.

Example
package main

import (
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	m := map[string]interface{}{
		"ints": []interface{}{
			1, 2,
		},
	}
	err := dyno.AppendMore(m, []interface{}{3, 4, 5}, "ints")
	fmt.Println(m, err)

}
Output:

map[ints:[1 2 3 4 5]] <nil>

func ConvertMapI2MapS

func ConvertMapI2MapS(v interface{}) interface{}

ConvertMapI2MapS walks the given dynamic object recursively, and converts maps with interface{} key type to maps with string key type. This function comes handy if you want to marshal a dynamic object into JSON where maps with interface{} key type are not allowed.

Recursion is implemented into values of the following types:

-map[interface{}]interface{}
-map[string]interface{}
-[]interface{}

When converting map[interface{}]interface{} to map[string]interface{}, fmt.Sprint() with default formatting is used to convert the key to a string key.

Example
package main

import (
	"encoding/json"
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	m := map[interface{}]interface{}{
		1:         "one",
		"numbers": []interface{}{2, 3, 4.4},
	}

	// m cannot be marshaled using encoding/json:
	data, err := json.Marshal(m)
	fmt.Printf("JSON: %q, error: %v\n", data, err)

	m2 := dyno.ConvertMapI2MapS(m)

	// But m2 can be:
	data, err = json.Marshal(m2)
	fmt.Printf("JSON: %s, error: %v\n", data, err)

}
Output:

JSON: "", error: json: unsupported type: map[interface {}]interface {}
JSON: {"1":"one","numbers":[2,3,4.4]}, error: <nil>

func Delete

func Delete(v interface{}, key interface{}, path ...interface{}) error

Delete deletes a key from a map or an element from a slice denoted by the path.

Deleting a non-existing map key is a no-op. Attempting to delete a slice element from a slice with invalid index is an error.

Path cannot be empty or nil if v itself is a slice, else an error is returned.

Example
package main

import (
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	m := map[string]interface{}{
		"name": "Bob",
		"ints": []interface{}{
			1, 2, 3,
		},
	}

	err := dyno.Delete(m, "name")
	fmt.Println(m, err)

	err = dyno.Delete(m, 1, "ints")
	fmt.Println(m, err)

	err = dyno.Delete(m, "ints")
	fmt.Println(m, err)

}
Output:

map[ints:[1 2 3]] <nil>
map[ints:[1 3]] <nil>
map[] <nil>

func Get

func Get(v interface{}, path ...interface{}) (interface{}, error)

Get returns a value denoted by the path.

If path is empty or nil, v is returned.

Example
package main

import (
	"fmt"

	"github.com/icza/dyno"
)

func main() {
	m := map[string]interface{}{
		"a": 1,
		"b": map[interface{}]interface{}{
			3: []interface{}{1, "two", 3.3},
		},
	}

	printValue := func(v interface{}, err error) {
		fmt.Printf("Value: %-5v, Error: %v\n", v, err)
	}

	printValue(dyno.Get(m, "a"))
	printValue(dyno.Get(m, "b", 3, 1))
	printValue(dyno.Get(m, "x"))

	sl, _ := dyno.Get(m, "b", 3) // This is: []interface{}{1, "two", 3.3}
	printValue(dyno.Get(sl, 4))

}
Output:

Value: 1    , Error: <nil>
Value: two  , Error: <nil>
Value: <nil>, Error: missing key: x (path element idx: 0)
Value: <nil>, Error: index out of range: 4 (path element idx: 0)

func GetBoolean

func GetBoolean(v interface{}, path ...interface{}) (bool, error)

GetBoolean returns a bool value denoted by the path.

This function accepts many different types and converts them to bool, namely:

-boolean type
-integer and floating point types (false for zero values, true otherwise)
-string (fmt.Sscan() will be used for parsing)

If path is empty or nil, v is returned as a bool.

func GetFloat64

func GetFloat64(v interface{}, path ...interface{}) (float64, error)

GetFloat64 returns a float64 value denoted by the path.

If path is empty or nil, v is returned as a float64.

func GetFloating

func GetFloating(v interface{}, path ...interface{}) (float64, error)

GetFloating returns a float64 value denoted by the path.

This function accepts many different types and converts them to float64, namely:

-floating point types (float64, float32)
-integer types (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64)
 (which implies the aliases byte and rune too)
-string (fmt.Sscan() will be used for parsing)
-any type with a Float64() (float64, error) method (e.g. json.Number)

If path is empty or nil, v is returned as an int64.

func GetInt

func GetInt(v interface{}, path ...interface{}) (int, error)

GetInt returns an int value denoted by the path.

If path is empty or nil, v is returned as an int.

func GetInteger

func GetInteger(v interface{}, path ...interface{}) (int64, error)

GetInteger returns an int64 value denoted by the path.

This function accepts many different types and converts them to int64, namely:

-integer types (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64)
 (which implies the aliases byte and rune too)
-floating point types (float64, float32)
-string (fmt.Sscan() will be used for parsing)
-any type with an Int64() (int64, error) method (e.g. json.Number)

If path is empty or nil, v is returned as an int64.

func GetMapI

func GetMapI(v interface{}, path ...interface{}) (map[interface{}]interface{}, error)

GetMapI returns a map with interface{} keys denoted by the path.

If path is empty or nil, v is returned as a slice.

func GetMapS

func GetMapS(v interface{}, path ...interface{}) (map[string]interface{}, error)

GetMapS returns a map with string keys denoted by the path.

If path is empty or nil, v is returned as a slice.

func GetSlice

func GetSlice(v interface{}, path ...interface{}) ([]interface{}, error)

GetSlice returns a slice denoted by the path.

If path is empty or nil, v is returned as a slice.

func GetString

func GetString(v interface{}, path ...interface{}) (string, error)

GetString returns a string value denoted by the path.

If path is empty or nil, v is returned as a string.

func SGet

func SGet(m map[string]interface{}, path ...string) (interface{}, error)

SGet returns a value denoted by the path consisting of only string keys.

SGet is an optimized and specialized version of the general Get. The path may only contain string map keys (no slice indices), and each value associated with the keys (being the path elements) must also be maps with string keys, except the value asssociated with the last path element.

If path is empty or nil, m is returned.

func SSet

func SSet(m map[string]interface{}, value interface{}, path ...string) error

SSet sets a map element with string key type, denoted by the path consisting of only string keys.

SSet is an optimized and specialized version of the general Set. The path may only contain string map keys (no slice indices), and each value associated with the keys (being the path elements) must also be a maps with string keys, except the value associated with the last path element.

The map denoted by the preceding path before the last path element must already exist.

Path cannot be empty or nil, else an error is returned.

func Set

func Set(v interface{}, value interface{}, path ...interface{}) error

Set sets a map or slice element denoted by the path.

The last element of the path must be a map key or a slice index, and the preceding path must denote a map or a slice respectively which must already exist.

Path cannot be empty or nil, else an error is returned.

Example
package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/icza/dyno"
)

func main() {
	m := map[string]interface{}{
		"a": 1,
		"b": map[string]interface{}{
			"3": []interface{}{1, "two", 3.3},
		},
	}

	printMap := func(err error) {
		json.NewEncoder(os.Stdout).Encode(m) // Output JSON
		if err != nil {
			fmt.Println("ERROR:", err)
		}
	}

	printMap(dyno.Set(m, 2, "a"))
	printMap(dyno.Set(m, "owt", "b", "3", 1))
	printMap(dyno.Set(m, 1, "x"))

	sl, _ := dyno.Get(m, "b", "3") // This is: []interface{}{1, "owt", 3.3}
	printMap(dyno.Set(sl, 1, 4))

}
Output:

{"a":2,"b":{"3":[1,"two",3.3]}}
{"a":2,"b":{"3":[1,"owt",3.3]}}
{"a":2,"b":{"3":[1,"owt",3.3]},"x":1}
{"a":2,"b":{"3":[1,"owt",3.3]},"x":1}
ERROR: index out of range: 4 (path element idx: 0)

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL