sheriff

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2024 License: BSD-3-Clause Imports: 6 Imported by: 0

README

sheriff

GoDoc Build Status Coverage Status

go get github.com/liip/sheriff/v2

Package sheriff marshals structs conditionally based on tags on the fields.

A typical use is an API which marshals structs into JSON and maintains different API versions. Using sheriff, struct fields can be annotated with API version and group tags.

By invoking sheriff with specific options, those tags determine whether a field will be added to the output map or not. It can then be marshalled using "encoding/json".

NOTE: This package is tested only on Go 1.7+, it might work on Go 1.6 too, but no support is given.

Implemented tags

Groups

Groups can be used for limiting the output based on freely defined parameters. For example: restrict marshalling the email address of a user to the user itself by just adding the group personal if the user fetches his profile. Multiple groups can be separated by comma.

Example:

type GroupsExample struct {
    Username      string `json:"username" groups:"api"`
    Email         string `json:"email" groups:"personal"`
    SomethingElse string `json:"something_else" groups:"api,personal"`
}
Anonymous fields

Tags added to a struct’s anonymous field propagates to the inner-fields if no other tags are specified.

Example:

type UserInfo struct {
    UserPrivateInfo `groups:"private"`
    UserPublicInfo  `groups:"public"`
}
type UserPrivateInfo struct {
    Age string
}
type UserPublicInfo struct {
    ID    string
    Email string
}
Since

Since specifies the version since that field is available. It's inclusive and SemVer compatible using github.com/hashicorp/go-version. If you specify version 2 in a tag, this version will be output in case you specify version >=2.0.0 as the API version.

Example:

type SinceExample struct {
    Username string `json:"username" since:"2.1.0"`
    Email    string `json:"email" since:"2"`
}
Until

Until specifies the version until that field is available. It's the opposite of since, inclusive and SemVer compatible using github.com/hashicorp/go-version. If you specify version 2 in a tag, this version will be output in case you specify version <=2.0.0 as the API version.

Example:

type UntilExample struct {
    Username string `json:"username" until:"2.1.0"`
    Email    string `json:"email" until:"2"`
}

Example

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/hashicorp/go-version"
	"github.com/liip/sheriff/v2"
)

type User struct {
	Username string   `json:"username" groups:"api"`
	Email    string   `json:"email" groups:"personal"`
	Name     string   `json:"name" groups:"api"`
	Roles    []string `json:"roles" groups:"api" since:"2"`
}

type UserList []User

func MarshalUsers(version *version.Version, groups []string, users UserList) ([]byte, error) {
	o := &sheriff.Options{
		Groups:     groups,
		ApiVersion: version,
	}

	data, err := sheriff.Marshal(o, users)
	if err != nil {
		return nil, err
	}

	return json.MarshalIndent(data, "", "  ")
}

func main() {
	users := UserList{
		User{
			Username: "alice",
			Email:    "alice@example.org",
			Name:     "Alice",
			Roles:    []string{"user", "admin"},
		},
		User{
			Username: "bob",
			Email:    "bob@example.org",
			Name:     "Bob",
			Roles:    []string{"user"},
		},
	}

	v1, err := version.NewVersion("1.0.0")
	if err != nil {
		log.Panic(err)
	}
	v2, err := version.NewVersion("2.0.0")

	output, err := MarshalUsers(v1, []string{"api"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 1 output:")
	fmt.Printf("%s\n\n", output)

	output, err = MarshalUsers(v2, []string{"api"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 2 output:")
	fmt.Printf("%s\n\n", output)

	output, err = MarshalUsers(v2, []string{"api", "personal"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 2 output with personal group too:")
	fmt.Printf("%s\n\n", output)

}

// Output:
// Version 1 output:
// [
//   {
//     "name": "Alice",
//     "username": "alice"
//   },
//   {
//     "name": "Bob",
//     "username": "bob"
//   }
// ]
//
// Version 2 output:
// [
//   {
//     "name": "Alice",
//     "roles": [
//       "user",
//       "admin"
//     ],
//     "username": "alice"
//   },
//   {
//     "name": "Bob",
//     "roles": [
//       "user"
//     ],
//     "username": "bob"
//   }
// ]
//
// Version 2 output with personal group too:
// [
//   {
//     "email": "alice@example.org",
//     "name": "Alice",
//     "roles": [
//       "user",
//       "admin"
//     ],
//     "username": "alice"
//   },
//   {
//     "email": "bob@example.org",
//     "name": "Bob",
//     "roles": [
//       "user"
//     ],
//     "username": "bob"
//   }
// ]

Output ordering

Sheriff converts the input struct into a basic structure using map[string]interface{}. This means that the generated JSON will not have the same ordering as the input struct. If you need to have a specific ordering then a custom implementation of the KVStoreFactory can be passed as an option.

Providing a custom KV Store is likely to have a negative impact on performance, as such it should be used only when necessary.

For example:

package main

import (
	"github.com/liip/sheriff/v2"
	orderedmap "github.com/wk8/go-ordered-map/v2"
)

type OrderedMap struct {
	*orderedmap.OrderedMap[string, interface{}]
}

func NewOrderedMap() *OrderedMap {
	return &OrderedMap{orderedmap.New[string, interface{}]()}
}

func (om *OrderedMap) Set(k string, v interface{}) {
	om.OrderedMap.Set(k, v)
}

func (om *OrderedMap) Each(f func(k string, v interface{})) {
	for pair := om.Newest(); pair != nil; pair = pair.Prev() {
		f(pair.Key, pair.Value)
	}
}

func main() {
	opt := &sheriff.Options{
		KVStoreFactory: func() sheriff.KVStore {
			return NewOrderedMap()
		},
	}

	// ...
}

Benchmarks

There's a simple benchmark in bench_test.go which compares running sheriff -> JSON versus just marshalling into JSON and runs on every build. Just marshalling JSON itself takes usually between 3 and 5 times less nanoseconds per operation compared to running sheriff and JSON.

Want to make sheriff faster? Please send us your pull request or open an issue discussing a possible improvement 🚀!

Acknowledgements

  • This idea and code has been created partially during a Liip hackday.
  • Thanks to @basgys for reviews & improvements.

mweibel/php-to-go is a code generator translating PHP models (using JMS serializer) to Go structs with sheriff tags. The two projects were initially developed together and just recently php-to-go has been split out and published too.

Documentation

Overview

Package sheriff transforms structs into a map based on specific tags on the struct fields. A typical use is an API which marshals structs into JSON and maintains different API versions. Using sheriff, struct fields can be annotated with API version and group tags. By invoking sheriff with specific options, those tags determine whether a field will be added to the output map or not. It can then be marshalled using "encoding/json".

Example
package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/hashicorp/go-version"
	"github.com/liip/sheriff/v2"
)

type User struct {
	Username string   `json:"username" groups:"api"`
	Email    string   `json:"email" groups:"personal"`
	Name     string   `json:"name" groups:"api"`
	Roles    []string `json:"roles" groups:"api" since:"2"`
}

type UserList []User

func MarshalUsers(version *version.Version, groups []string, users UserList) ([]byte, error) {
	o := &sheriff.Options{
		Groups:     groups,
		ApiVersion: version,
	}

	data, err := sheriff.Marshal(o, users)
	if err != nil {
		return nil, err
	}

	return json.MarshalIndent(data, "", "  ")
}

func main() {
	users := UserList{
		User{
			Username: "alice",
			Email:    "alice@example.org",
			Name:     "Alice",
			Roles:    []string{"user", "admin"},
		},
		User{
			Username: "bob",
			Email:    "bob@example.org",
			Name:     "Bob",
			Roles:    []string{"user"},
		},
	}

	v1, err := version.NewVersion("1.0.0")
	if err != nil {
		log.Panic(err)
	}
	v2, err := version.NewVersion("2.0.0")
	if err != nil {
		log.Panic(err)
	}

	output, err := MarshalUsers(v1, []string{"api"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 1 output:")
	fmt.Printf("%s\n\n", output)

	output, err = MarshalUsers(v2, []string{"api"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 2 output:")
	fmt.Printf("%s\n\n", output)

	output, err = MarshalUsers(v2, []string{"api", "personal"}, users)
	if err != nil {
		log.Panic(err)
	}
	fmt.Println("Version 2 output with personal group too:")
	fmt.Printf("%s\n\n", output)

}
Output:

Version 1 output:
[
  {
    "name": "Alice",
    "username": "alice"
  },
  {
    "name": "Bob",
    "username": "bob"
  }
]

Version 2 output:
[
  {
    "name": "Alice",
    "roles": [
      "user",
      "admin"
    ],
    "username": "alice"
  },
  {
    "name": "Bob",
    "roles": [
      "user"
    ],
    "username": "bob"
  }
]

Version 2 output with personal group too:
[
  {
    "email": "alice@example.org",
    "name": "Alice",
    "roles": [
      "user",
      "admin"
    ],
    "username": "alice"
  },
  {
    "email": "bob@example.org",
    "name": "Bob",
    "roles": [
      "user"
    ],
    "username": "bob"
  }
]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Marshal

func Marshal(options *Options, data interface{}) (interface{}, error)

Marshal encodes the passed data into a map which can be used to pass to json.Marshal().

If the passed argument `data` is a struct, the return value will be of type `map[string]interface{}`. In all other cases we can't derive the type in a meaningful way and is therefore an `interface{}`.

Types

type FieldFilter

type FieldFilter func(field reflect.StructField) (bool, error)

A FieldFilter is a function that decides whether a field should be marshalled or not. If it returns true, the field will be marshalled, otherwise it will be skipped.

type KVStore

type KVStore interface {
	Set(k string, v interface{})
	Each(f func(k string, v interface{}))
}

A KVStore is a simple key-value store. The default implementation uses a map[string]interface{}. This is fast, however will lead to inconsistent ordering of the keys in the generated JSON. A custom implementation can be used to maintain the order of the keys.

type MarshalInvalidTypeError

type MarshalInvalidTypeError struct {
	// contains filtered or unexported fields
}

MarshalInvalidTypeError is an error returned to indicate the wrong type has been passed to Marshal.

func (MarshalInvalidTypeError) Error

func (e MarshalInvalidTypeError) Error() string

type Marshaller

type Marshaller interface {
	Marshal(options *Options) (interface{}, error)
}

Marshaller is the interface models have to implement in order to conform to marshalling.

type Options

type Options struct {
	// The FieldFilter makes the decision whether a field should be marshalled or not.
	// It receives the reflect.StructField of the field and should return true if the field should be included.
	// If this is not set then the default FieldFilter will be used, which uses the Groups and ApiVersion fields.
	// Setting this value will result in the other options being ignored.
	FieldFilter FieldFilter

	// Groups determine which fields are getting marshalled based on the groups tag.
	// A field with multiple groups (comma-separated) will result in marshalling of that
	// field if one of their groups is specified.
	Groups []string
	// ApiVersion sets the API version to use when marshalling.
	// The tags `since` and `until` use the API version setting.
	// Specifying the API version as "1.0.0" and having an until setting of "2"
	// will result in the field being marshalled.
	// Specifying a since setting of "2" with the same API version specified,
	// will not marshal the field.
	ApiVersion *version.Version
	// IncludeEmptyTag determines whether a field without the
	// `groups` tag should be marshalled ot not.
	// This option is false by default.
	IncludeEmptyTag bool

	// The KVStoreFactory is a function that returns a new KVStore.
	// The default implementation uses a map[string]interface{}, which is fast but does not maintain the order of the
	// keys.
	// A custom implementation can be used to maintain the order of the keys, i.e. using github.com/wk8/go-ordered-map
	KVStoreFactory func() KVStore
	// contains filtered or unexported fields
}

Options determine which struct fields are being added to the output map.

Jump to

Keyboard shortcuts

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