owl

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Dec 30, 2023 License: MIT Imports: 9 Imported by: 1

README

Owl

Owl is a Go package that provides algorithmic guidance through structured directives using Go struct tags.

Go codecov Go Report Card Go Reference

Documentation

Overview

Package "owl" a go struct tag framework and algorithm driver. It provides algorithmic guidance through structured directives using Go struct tags. Users can design their own directives and register them to owl. Owl will run the directives on the struct fields in a depth-first manner. Which means it is suitable for data-binding algorithms, data validations, etc.

Index

Constants

View Source
const DefaultTagName = "owl"

Variables

View Source
var (
	ErrUnsupportedType      = errors.New("unsupported type")
	ErrInvalidDirectiveName = errors.New("invalid directive name")
	ErrDuplicateDirective   = errors.New("duplicate directive")
	ErrMissingExecutor      = errors.New("missing executor")
	ErrTypeMismatch         = errors.New("type mismatch")
	ErrScanNilField         = errors.New("scan nil field")
)

Functions

func RegisterDirectiveExecutor

func RegisterDirectiveExecutor(name string, exe DirectiveExecutor, replace ...bool)

RegisterDirectiveExecutor registers a named executor globally, i.e. to the default namespace.

func Tag

func Tag() string

Tag returns the tag name where the directives are parsed from.

func UseTag

func UseTag(tag string)

UseTag sets the tag name to parse directives.

Types

type Directive

type Directive struct {
	Name string   // name of the executor
	Argv []string // argv
}

Directive defines the profile to locate a `DirectiveExecutor` instance and drives it with essential arguments.

func NewDirective

func NewDirective(name string, argv ...string) *Directive

NewDirective creates a Directive instance.

func ParseDirective

func ParseDirective(directive string) (*Directive, error)

ParseDirective creates a Directive instance by parsing a directive string extracted from the struct tag.

Example directives are:

"form=page,page_index" -> { Name: "form", Args: ["page", "page_index"] }
"header=x-api-token"   -> { Name: "header", Args: ["x-api-token"] }

func (*Directive) Copy added in v0.7.0

func (d *Directive) Copy() *Directive

Copy creates a copy of the directive. The copy is a deep copy.

func (*Directive) String added in v0.7.0

func (d *Directive) String() string

String returns the string representation of the directive.

type DirectiveExecutionError

type DirectiveExecutionError struct {
	Err error
	Directive
}

func (*DirectiveExecutionError) Error

func (e *DirectiveExecutionError) Error() string

func (*DirectiveExecutionError) Unwrap

func (e *DirectiveExecutionError) Unwrap() error

type DirectiveExecutor

type DirectiveExecutor interface {
	Execute(*DirectiveRuntime) error
}

DirectiveExecutor is the interface that wraps the Execute method. Execute executes the directive by passing the runtime context.

func LookupExecutor

func LookupExecutor(name string) DirectiveExecutor

LookupExecutor returns the executor by name globally (from the default namespace).

type DirectiveExecutorFunc

type DirectiveExecutorFunc func(*DirectiveRuntime) error

DirecrtiveExecutorFunc is an adapter to allow the use of ordinary functions as DirectiveExecutors.

func (DirectiveExecutorFunc) Execute

type DirectiveRunOrder added in v0.6.0

type DirectiveRunOrder func(*Directive, *Directive) bool

type DirectiveRuntime

type DirectiveRuntime struct {
	Directive *Directive
	Resolver  *Resolver

	// Value is the reflect.Value of the field that the directive is applied to.
	// Worth noting that the value is a pointer to the field value. Which means
	// if the field is of type int. Then Value is of type *int. And the actual
	// value is stored in Value.Elem().Int(). The same for other types. So if you
	// want to modify the field value, you should call Value.Elem().Set(value),
	// e.g. Value.Elem().SetString(value), Value.Elem().SetInt(value), etc.
	Value reflect.Value

	// Context is the runtime context of the directive execution. The initial
	// context can be tweaked by applying options to Resolver.Resolve method.
	// Use WithValue option to set a value to the initial context. Each field
	// resolver will creates a new context by copying this initial context. And
	// for the directives of the same field resolver, they will use the same
	// context. Latter directives can use the values set by the former
	// directives. Ex:
	//
	//  type User struct {
	//      Name 	string `owl:"dirA;dirB"` // Context_1
	//      Gender 	string `owl:"dirC"`      // Context_2
	//  }
	//
	//  New(User{}).Resolve(WithValue("color", "red")) // Context_0
	//
	// In the above example, the initial context is Context_0. It has a value of "color" set to "red".
	// The context of the first field resolver is Context_1. It is created by copying Context_0.
	// The context of the second field resolver is Context_2. It is also created by copying Context_0.
	// Thus, all the directives during execution can access the value of "color" in Context_0.
	// For the Name field resolver, it has two directives, dirA and dirB. They will use the same context Context_1.
	// and if in dirA we set a value of "foo" to "bar", then in dirB we can get the value of "foo" as "bar".
	Context context.Context
}

DirectiveRuntime is the execution runtime/context of a directive. NOTE: the Directive and Resolver are both exported for the convenience but in an unsafe way. The user should not modify them. If you want to modify them, please call Resolver.Iterate to iterate the resolvers and modify them in the callback. And make sure this be done before any callings to Resolver.Resolve.

type Namespace

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

Namespace isolates the executors as a collection.

func NewNamespace

func NewNamespace() *Namespace

NewNamespace creates a new namespace. Which is a collection of executors.

func (*Namespace) LookupExecutor

func (ns *Namespace) LookupExecutor(name string) DirectiveExecutor

LookupExecutor returns the executor by name.

func (*Namespace) RegisterDirectiveExecutor

func (ns *Namespace) RegisterDirectiveExecutor(name string, exe DirectiveExecutor, replace ...bool)

RegisterDirectiveExecutor registers a named executor to the namespace. The executor should implement the DirectiveExecutor interface. Will panic if the name were taken or the executor is nil. Pass replace (true) to ignore the name conflict.

type Option

type Option interface {
	Apply(context.Context) context.Context
}

Option is an option for New.

func WithDirectiveRunOrder added in v0.6.0

func WithDirectiveRunOrder(runOrder DirectiveRunOrder) Option

WithDirectiveRunOrder sets the order of execution for directives.

When used in New, the directives will be sorted at the tree building stage.

When Used in Resolve or Scan, a copy of the directives will be sorted and used.

func WithNamespace

func WithNamespace(ns *Namespace) Option

WithNamespace binds a namespace to the resolver. The namespace is used to lookup directive executors. There's a default namespace, which is used when the namespace is not specified. The namespace set in New will be overridden by the namespace set in Resolve or Scan.

func WithNestedDirectivesEnabled added in v0.5.0

func WithNestedDirectivesEnabled(resolve bool) Option

WithNestedDirectivesEnabled controls whether to resolve nested directives. The default value is true. When set to false, the nested directives will not be executed. The value set in New will be overridden by the value set in Resolve or Scan.

func WithValue

func WithValue(key, value interface{}) Option

WithValue binds a value to the context.

When used in New, the value is bound to Resolver.Context.

When used in Resolve or Scan, the value is bound to DirectiveRuntime.Context. See DirectiveRuntime.Context for more details.

type OptionFunc

type OptionFunc func(context.Context) context.Context

OptionFunc is a function that implements Option.

func (OptionFunc) Apply

func (f OptionFunc) Apply(ctx context.Context) context.Context

type ResolveError

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

func (*ResolveError) AsDirectiveExecutionError added in v0.1.6

func (e *ResolveError) AsDirectiveExecutionError() *DirectiveExecutionError

func (*ResolveError) Error

func (e *ResolveError) Error() string

func (*ResolveError) Unwrap

func (e *ResolveError) Unwrap() error

type Resolver

type Resolver struct {
	Type       reflect.Type
	Field      reflect.StructField
	Index      []int
	Path       []string
	Directives []*Directive
	Parent     *Resolver
	Children   []*Resolver
	Context    context.Context // save custom resolver settings here
}

Resolver is a field resolver. Which is a node in the resolver tree. The resolver tree is built from a struct value. Each node represents a field in the struct. The root node represents the struct itself. It is used to resolve a field value from a data source.

func New

func New(structValue interface{}, opts ...Option) (*Resolver, error)

New builds a resolver tree from a struct value. The given options will be applied to all the resolvers. In the resolver tree, each node is also a Resolver.

func (*Resolver) Copy added in v0.7.0

func (r *Resolver) Copy() *Resolver

Copy returns a copy of the resolver tree. The copy is a deep copy, which means the children are also copied.

func (*Resolver) DebugLayoutText

func (r *Resolver) DebugLayoutText(depth int) string

func (*Resolver) GetDirective

func (r *Resolver) GetDirective(name string) *Directive

func (*Resolver) IsLeaf

func (r *Resolver) IsLeaf() bool

func (*Resolver) IsRoot

func (r *Resolver) IsRoot() bool

func (*Resolver) Iterate

func (r *Resolver) Iterate(fn func(*Resolver) error) error

Iterate visits the resolver tree by depth-first. The callback function will be called on each field resolver. The iteration will stop if the callback returns an error.

func (*Resolver) Lookup

func (r *Resolver) Lookup(path string) *Resolver

Find finds a field resolver by path. e.g. "Pagination.Page", "User.Name", etc.

func (*Resolver) Namespace

func (r *Resolver) Namespace() *Namespace

func (*Resolver) PathString

func (r *Resolver) PathString() string

func (*Resolver) RemoveDirective added in v0.1.4

func (r *Resolver) RemoveDirective(name string) *Directive

func (*Resolver) Resolve

func (r *Resolver) Resolve(opts ...Option) (reflect.Value, error)

Resolve resolves the struct type by traversing the tree in depth-first order. Typically it is used to create a new struct instance by reading from some data source. This method always creates a new value of the type the resolver holds. And runs the directives on each field.

Use WithValue to create an Option that can add custom values to the context, the context can be used by the directive executors during the resolution. Example:

type Settings struct {
  DarkMode bool `owl:"env=MY_APP_DARK_MODE;cfg=appearance.dark_mode;default=false"`
}
resolver := owl.New(Settings{})
settings, err := resolver.Resolve(WithValue("app_config", appConfig))

NOTE: while iterating the tree, if resolving a field fails, the iteration will be stopped and the error will be returned.

func (*Resolver) Scan added in v0.2.0

func (r *Resolver) Scan(value any, opts ...Option) error

Scan scans the struct value by traversing the fields in depth-first order. The value is required to have the same type as the resolver holds. While scanning, it will run the directives on each field. The DirectiveRuntime that can be accessed during the directive exeuction will have its Value property populated with reflect.Value of the field. Typically, Scan is used to do some readonly operations against the struct value, e.g. validate the struct value, build something based on the struct value, etc.

Use WithValue to create an Option that can add custom values to the context, the context can be used by the directive executors during the resolution.

NOTE: Unlike Resolve, it will iterate the whole resolver tree against the given value, try to access each corresponding field. Even scan fails on one of the fields, it will continue to scan the rest of the fields. The returned error can be a multi-error combined by errors.Join, which contains all the errors that occurred during the scan.

func (*Resolver) String

func (r *Resolver) String() string

type ScanError added in v0.2.0

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

func (*ScanError) AsDirectiveExecutionError added in v0.2.0

func (e *ScanError) AsDirectiveExecutionError() *DirectiveExecutionError

func (*ScanError) Error added in v0.2.0

func (e *ScanError) Error() string

func (*ScanError) Unwrap added in v0.2.0

func (e *ScanError) Unwrap() error

Jump to

Keyboard shortcuts

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