rs

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 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 there 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.

Simple and straightforward, right?

Why?

Both writing and resolving software configuration are tedious and error prone, especially when you want to achieve felixibility to some extent.

Here is an example, given target config like this

type Config struct {
    A string
    B int
    C float64

    // RemoteHostScript for remote host execution
    RemoteHostScript string `yaml:"remote_host_script"`
}

when you want to support environment variables for its fields A, B, C, while not including remote_host_script (obviously the remote_host_script should be executed in some remote system), what whould you do?

a: ${ENV_FOR_A}
b: ${ENV_FOR_B}
c: ${ENV_FOR_C}

remote_host_script: |-
  #!/bin/sh

  echo ${ENV_WITH_DIFFERENT_CONTEXT}
  • Make remote_host_script a file path reference (or add a new field like remote_host_script_file)?
    • Simple and effective, but now you have two source of remote host script, more fixed code logic added, more document to come for preferred options when both is set. End user MUST read your documentation in detail for such subtle issues (if you are good at documenting).
  • Expand environment variables before unmarshaling?
    • What would you do with ${ENV_WITH_DIFFERENT_CONTEXT}? Well, you can unmarshal the config into two config, one with environment variables expanded, another not, and merge them into one.
  • Unmarshal yaml as map[string]interface{} first, then do custom handling for every field?
    • Now you have to work with types manually, tedious yet error prone job starts now.
  • Create a new DSL, add some keywords...
    • We have already seen so many DSLs created just for configuration purpose, almost none of them really simplified the configuration management, and usually only useful for development not deployment.

As developers, what we actually need is let end user decide which field is resolved by what method, and we just control when to resolve which field.

Rendering suffix is applicable to every single yaml field, doing exactly what end user need, and it can resolve fields partialy with some strategy in code, also exactly what developers want.

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)
    • 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 code
  • Vanilla yaml, valid for all standard yaml parser
  • Everything gopkg.in/yaml.v3 supports are supported
    • Anchors, Alias, YAML Merges

Sample YAML Doc with all features above

foo@a|b|c!: &foo
  value@http?[]obj: https://example.com
  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|b|c: *foo

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.

  • Built-in map data structure with rendering suffix applied to map key are treated as is, rendering suffix won't be recognized.
    • which means for map[string]interface{}, foo@foo: bar is just a map item with key foo@foo, value bar, no data to be resolved

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

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() 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

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. nolint:gocyclo

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

	// TagNamespace used for struct field tag parsing
	// you can set it to json to use json tag as name source
	//
	// supported tag values are:
	// - <first tag value as data field name>
	// - inline
	// - omitempty
	//
	// unsupported tag values are ignored
	//
	// defaults to `yaml`
	TagNamespace 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