rs

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2021 License: Apache-2.0 Imports: 11 Imported by: 38

README

rs

CI PkgGoDev GoReportCard Coverage

Rendering Suffix Support for yaml

What?

Before we start, let's agree on the concept renderer is just a simple function that takes a input and generates a output.

input -> [ renderer ] -> output

A plain yaml doc looks like this:

foo: bar

Everything's static, once unmarshaled, foo gets value bar.

Now what if we would like to take an environment variable as foo's value?

foo: ${FOO}

you will only get ${FOO} after unmarshaling, you have to code you own logic to map ${FOO} to some system environment variable.

What you code, is actually a renderer with its usage specific to foo.

Rendering suffix is here to help, it offers a way to make your yaml doc dynamic on its own, you control what you get in yaml rather than your compiled code.

How it looks?

Let's continue with the environment variable support, say we have developed a renderer env (which calls os.ExpandEnv to map env reference to its value)

foo@env: ${FOO}

Before I would start, probably you have already figured out what's going on:

@ is like a notifier, and notifies the renderer env with value ${FOO}, then env does its own job and generates some output with os.ExpandEnv function call, and at last, the output is set as foo's value.

But also wondering how can foo@env be resolved as foo since they are completely different field name! That's what we are taking care of, see How it works? section for brief introduction.

It's simple and straightforward, right?

Prerequisites

  • Use gopkg.in/yaml.v3 for yaml marshaling/unmarshaling

Features

  • Field specific rendering: customizable in-place yaml data rendering
    • Add a suffix starts with @, followed by some renderer name, to your yaml field name as rendering suffix (e.g. foo@<renderer-name>: bar)
  • Type hinting: keep you data as what it supposed to be
    • Add a type hint suffix ?<some-type> to your renderer (e.g. foo@<renderer-name>?[]obj: bar suggests foo should be an array of objects using result from <renderer-name> generated with input bar)
    • See list of supported type hints
  • Data merging and patching made esay: create patching spec in yaml doc
    • Add a patching suffix ! to your renderer (after the type hint if any), feed it a patch spec object
  • Renderer chaining: render you data with a rendering pipeline
    • join you renderers with pipes (|), get your data rendered through the pipeline (e.g. join three renderers a, b, c -> a|b|c)
  • Supports arbitraty yaml doc without type definition in your own code.
  • Everything gopkg.in/yaml.v3 supports are supported.
    • Anchors, Alias, YAML Merges ...
  • Extended but still vanilla yaml, your yaml doc stays valid for all standard yaml parser.

Sample YAML Doc with all features above

foo@a!: &foo
  value@env|http?[]obj: https://example.com/${FOO_FILE_PATH}
  merge:
  - value@file: ./value-a.yml
    select: |-
      [ .[] | sort ]
  patch:
  - { op: remove, path: /0/foo }
  - op: add
    path: /0/foo
    value: "bar"
    select: '. + "-suffix-from-jq-query"'
  select: |-
    { foo: .[0].foo, bar: .[1].bar }

bar@a!: *foo

NOTE: This module provides no renderer implementation, and the only built-in renderer is a pseudo renderer with empty name that skips rendering (output is what input is) for data patching and type hinting purpose (e.g. foo@?int!: { ... patch spec ... }). You have to roll out your own renderers.

Usage

See example_test.go

NOTE: You can find more examples in arhat-dev/dukkha

Known Limitations

See known_limitation_test.go for sample code and workaround.

  • Golang built-in map with rendering suffix applied to map key are treated as is, rendering suffix won't be recognized.
    • For map[string]interface{}, foo@foo: bar is just a map item with key foo@foo, value bar, no data to be resolved.
    • The reason for this limitation is obvious since built-in map types doesn't have BaseField embedded, but it can be counterintuitive when you have a map field in a struct having BaseField embedded.

How it works?

IoC (Inversion of Control) is famous for its application in DI (Dependency Injection), but what it literally says is to control the outside world at somewhere inside the world, that's the start point.

Custom yaml unmarshaling requires custom implementation of yaml.Unmarshaler, so usually you just get your structs unmarshaled by yaml.Unmarshal directly.

We implemented something called BaseField, it lives in your struct as a embedded field, all its methods are exposed to the outside world by default, and guess what, it implements yaml.Unmarshaler, so your struct implements yaml.Unmarshaler as well.

But can you control sibling fields in a struct? Not possible in golang unless with the help of outside world, that's why we need Init() function, calling Init() with your struct actually activates the BaseField in it, Init() function tells the inner BaseField what fields the parent struct have (its sibling fields), with the help of reflection.

You only have to call Init() once for the top level struct, since then the BaseField in it knows what to do with its sibling fields, it will also search for all structs with BaseField embedded when unmarshaling, call Init() for them, until the whole yaml doc is unmarshaled.

During the unmarshaling process, BaseField.UnmarshalYAML get called by yaml.Unmarhsal, it checks the input yaml field names, if a yaml field name has a suffix starting with @, then that yaml field will be treated as using rendering suffix, BaseField parses the yaml field name to know the real field name is (e.g. foo@bar's real field name is foo) and sets the rendering pipeline with the suffix, it also saves the yaml field value on its own but not setting the actual strcut field, when you call my_struct_with_BaseField.ResolveFields(), it feeds the rendering pipeline with saved field value to generate actual field value and set that as struct field value.

All in all, BaseField handles everything related to yaml unmarshaling to support rendering suffix after initial activation, so all you need to do is to embed a BaseField as the very first field in your struct where you want to support rendering suffix and activate the top level struct (with BaseField embedded, which can be some inner field) with a Init() function call.

FAQ

Have a look at FAQ.md or start/join a discussion on github.

LICENSE

Copyright 2021 The arhat.dev Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Documentation

Overview

Package rs provides rendering suffix support for yaml

Index

Examples

Constants

View Source
const (
	TagNameRS = "rs"
	TagName   = TagNameRS
)

Struct field tag names used by this package

Variables

View Source
var (
	ErrInterfaceTypeNotHandled = errors.New("interface type not handled")
)

Functions

func InitRecursively

func InitRecursively(fv reflect.Value, opts *Options)

InitRecursively trys to call Init on all fields implementing Field interface

func MergeMap

func MergeMap(
	original, additional map[string]interface{},

	appendList bool,
	uniqueInListItems bool,
) (map[string]interface{}, error)

func UniqueList added in v0.1.1

func UniqueList(dt []interface{}) []interface{}

Types

type AnyObject

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

AnyObject is a `interface{}` equivalent with rendering suffix support

func (*AnyObject) MarshalJSON

func (o *AnyObject) MarshalJSON() ([]byte, error)

func (*AnyObject) MarshalYAML

func (o *AnyObject) MarshalYAML() (interface{}, error)

func (*AnyObject) NormalizedValue added in v0.3.0

func (o *AnyObject) NormalizedValue() interface{}

NormalizedValue returns underlying value of the AnyObject with primitive types that is:

	for maps: map[string]interface{}
	for slices: []interface{}
 and primitive types for scalar types

should be called after fields being resolved

func (*AnyObject) ResolveFields

func (o *AnyObject) ResolveFields(rc RenderingHandler, depth int, fieldNames ...string) error

func (*AnyObject) UnmarshalYAML

func (o *AnyObject) UnmarshalYAML(n *yaml.Node) error

type AnyObjectMap added in v0.3.0

type AnyObjectMap struct {
	BaseField `yaml:"-" json:"-"`

	Data map[string]*AnyObject `rs:"other"`
}

AnyObjectMap is a `map[string]interface{}` equivalent with rendering suffix support

func (*AnyObjectMap) MarshalJSON added in v0.3.0

func (aom *AnyObjectMap) MarshalJSON() ([]byte, error)

func (*AnyObjectMap) MarshalYAML added in v0.3.0

func (aom *AnyObjectMap) MarshalYAML() (interface{}, error)

func (*AnyObjectMap) NormalizedValue added in v0.3.0

func (aom *AnyObjectMap) NormalizedValue() map[string]interface{}

NormalizedValue returns value of the AnyObjectMap with type map[string]interface{}

type BaseField

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

func (*BaseField) HasUnresolvedField

func (f *BaseField) HasUnresolvedField() bool

func (*BaseField) Inherit

func (f *BaseField) Inherit(b *BaseField) error

Inherit unresolved fields from another BaseField useful when you are merging two structs and want to resolve only once to get all unresolved fields set

after a successful function call, f wiil be able to resolve its struct fields with unresolved values from b and its own

func (*BaseField) MarshalYAML added in v0.4.0

func (f *BaseField) MarshalYAML() (interface{}, error)

func (*BaseField) ResolveFields

func (f *BaseField) ResolveFields(rc RenderingHandler, depth int, fieldNames ...string) error
Example
type MyStruct struct {
	rs.BaseField `yaml:"-"`

	MyValue string `yaml:"my_value"`
}

// required:
// initialize you data object
// before marshaling/unmarshaling/resolving
s := rs.Init(&MyStruct{}, nil).(*MyStruct)

// unmarshal yaml data using rendering suffix
err := yaml.Unmarshal([]byte(`{ my_value@my-renderer: 123 }`), s)
if err != nil {
	panic(err)
}

err = s.ResolveFields(
	// implement your own renderer
	rs.RenderingHandleFunc(
		func(renderer string, rawData interface{}) (result interface{}, err error) {
			switch dt := rawData.(type) {
			case string:
				return dt, nil
			default:
				return "hello", nil
			}
		},
	),
	-1,
)
if err != nil {
	panic(err)
}

fmt.Println(s.MyValue)
Output:

	hello

func (*BaseField) UnmarshalYAML

func (f *BaseField) UnmarshalYAML(n *yaml.Node) error

UnmarshalYAML handles parsing of rendering suffix and normal yaml unmarshaling

type Field

type Field interface {
	// A Field must implement yaml.Unmarshaler interface to process rendering suffix
	// in yaml key
	yaml.Unmarshaler

	// ResolveFields sets struct field values with data from unmarshaled yaml
	//
	// depth controls to what level this function call goes
	// when depth >= 1, resolve inner fields until reaching depth limit (depth == 0)
	// when depth == 0, do nothing
	// when depth < 0, resolve recursively
	//
	// fieldNames instructs which fields to be resolved. When it's not empty,
	// resolve specified fields only, otherwise, resolve all exported fields
	// in the underlying struct.
	ResolveFields(rc RenderingHandler, depth int, fieldNames ...string) error
}

Field defines methods required for rendering suffix support

these methods are implemented by BaseField, so usually you should just embed BaseField in your struct as the very first field

func Init

func Init(in Field, opts *Options) Field

Init the BaseField embedded in your struct, the BaseField must be the first field

type Foo struct {
	rs.BaseField // or *rs.BaseField
}

if the arg `in` doesn't contain BaseField or the BaseField is not the first element it does nothing and will return `in` as is.

type InterfaceTypeHandleFunc

type InterfaceTypeHandleFunc func(typ reflect.Type, yamlKey string) (interface{}, error)

InterfaceTypeHandleFunc is a helper type to wrap your function as InterfaceTypeHandler

func (InterfaceTypeHandleFunc) Create

func (f InterfaceTypeHandleFunc) Create(typ reflect.Type, yamlKey string) (interface{}, error)

type InterfaceTypeHandler

type InterfaceTypeHandler interface {
	// Create request interface type using yaml information
	Create(typ reflect.Type, yamlKey string) (interface{}, error)
}

InterfaceTypeHandler is used when setting values for interface{} typed field

type JSONPatchSpec

type JSONPatchSpec struct {
	BaseField `yaml:"-" json:"-"`

	Operation string `yaml:"op" json:"op"`

	Path string `yaml:"path" json:"path"`

	Value AnyObject `yaml:"value,omitempty" json:"value,omitempty"`

	// Select part of the value for patching
	//
	// this action happens before patching
	Select string `yaml:"select" json:"-"`
}

JSONPatchSpec per rfc6902

type MergeSource

type MergeSource struct {
	BaseField `yaml:"-" json:"-"`

	// Value for the source
	Value AnyObject `yaml:"value,omitempty"`

	// Select some data from the source
	Select string `yaml:"select,omitempty"`
}

type Options added in v0.4.0

type Options struct {
	// InterfaceTypeHandler handles interface value creation for any inner field
	// with a interface{...} type (including []interface{...} and map[string]interface{...})
	//
	// defaults to `nil`
	InterfaceTypeHandler InterfaceTypeHandler

	// DataTagNamespace used for struct field tag parsing
	// you can set it to json to use json tag
	//
	// supported tag values are:
	// - <first tag value as data field name>
	// - inline
	// - omitempty
	//
	// unsupported tag values are ignored
	//
	// defaults to `yaml`
	DataTagNamespace string

	// AllowUnknownFields whether restrict unmarshaling to known fields
	//
	// NOTE: if there is a map field in struct with field type `rs:"other"`
	// even when AllowUnknownFields is set to false, it still gets these unknown
	// fields marshaled to that map field
	//
	// defaults to `false`
	AllowUnknownFields bool
}

type PatchSpec

type PatchSpec struct {
	BaseField `yaml:"-" json:"-"`

	// Value for the renderer
	//
	// 	say we have a yaml list (`[bar]`) stored at https://example.com/bar.yaml
	//
	// 		foo@http!:
	// 		  value: https://example.com/bar.yaml
	// 		  merge: { value: [foo] }
	//
	// then the resolve value of foo will be `[bar, foo]`
	Value AnyObject `yaml:"value"`

	// Merge additional data into Value
	//
	// this action happens first
	Merge []MergeSource `yaml:"merge,omitempty"`

	// Patch Value using standard rfc6902 json-patch
	//
	// this action happens after merge
	Patch []JSONPatchSpec `yaml:"patch"`

	// Select part of the data as final result
	//
	// this action happens after merge and patch
	Select string `yaml:"select"`

	// Unique to make sure elements in the sequence is unique
	//
	// only effective when Value is yaml sequence
	Unique bool `yaml:"unique"`

	// MapListItemUnique to ensure items are unique in all merged lists respectively
	// lists with no merge data input are untouched
	MapListItemUnique bool `yaml:"map_list_item_unique"`

	// MapListAppend to append lists instead of replacing existing list
	MapListAppend bool `yaml:"map_list_append"`
}

PatchSpec is the input definition for renderers with a patching suffix

func (*PatchSpec) ApplyTo

func (s *PatchSpec) ApplyTo(valueData interface{}) (interface{}, error)

Apply Merge and Patch to Value, Unique is ensured if set to true

type RenderingHandleFunc added in v0.4.0

type RenderingHandleFunc func(renderer string, rawData interface{}) (result interface{}, err error)

RenderingHandleFunc is a helper type to wrap your function as RenderingHandler

func (RenderingHandleFunc) RenderYaml added in v0.4.0

func (f RenderingHandleFunc) RenderYaml(renderer string, rawData interface{}) (result interface{}, err error)

type RenderingHandler

type RenderingHandler interface {
	// RenderYaml transforms rawData to result through the renderer
	//
	// renderer is the name of your renderer without type hint (`?<hint>`) and patch suffix (`!`)
	//
	// rawData is the input to your renderer, which can be any type, your renderer is responsible
	// for casting it to its desired data type
	//
	// result can be any type as well, but if you returns an custom struct
	// you must make sure all desired value in it can be marshaled using yaml.Marshal
	RenderYaml(renderer string, rawData interface{}) (result interface{}, err error)
}

RenderingHandler is used when resolving yaml fields

type TypeHint added in v0.3.0

type TypeHint int8
const (
	TypeHintNone TypeHint = iota
	TypeHintStr
	TypeHintBytes
	TypeHintObject
	TypeHintObjects
	TypeHintInt
	TypeHintUint
	TypeHintFloat
)

func ParseTypeHint added in v0.3.0

func ParseTypeHint(h string) (TypeHint, error)

func (TypeHint) String added in v0.3.0

func (h TypeHint) String() string

Jump to

Keyboard shortcuts

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