Owl is a Go package that provides algorithmic guidance through structured directives using Go struct tags.
owl helps you implement functionalites which require leveraging Go struct tags, through:
- owl has defined a "standard syntax" to compose the tags, so you don't need to write code to parse tags;
- owl can scan the struct instance (recursively) for you, so you don't need to write code to iterate the struct by yourself, which requires a lot of reflection operations;
- you only need to write code to customize the behaviours of the tags, owl will call them during the scanning.
If you're still confused what owl can help you achieve, take a look at this tiny package: ggicci/goenv.
Customer | Achievement |
---|---|
httpin | owl helps httpin enable the mutual conversion between an HTTP request and a Go struct. |
Read Go struct tags if you have no idea what it is.
owl has a default tag named owl
, like json
for encoding/json
package. The default means without any modifications, owl will only extract the owl
tag that defined in the struct tags and parse it. You can use the following code to use any tag name you want:
owl.UseTag("mytag")
For example, in httpin
package, they use in
as the tag name.
type Authorization struct {
Token string `in:"query=access_token,token;header=x-api-token;required"`
^----------------------^ ^----------------^ ^------^
d1 d2 d3
}
In owl, a directive is a formatted string, consisting of two parts, the Directive Executor and the arguments, separated by an equal sign (=
), formatted as:
DirectiveExecutorName=ARGV
Which works like a concrete function call.
To the left of the =
is the name of the directive. There's a corresponding directive executor (with the same name) working under the hood.
To the right of the =
are the arguments, which will be passed to the algorithm at runtime. The way to define arguments can differ across different directives. In general, comma (,
) separated strings are used for multiple arguments. Arguments can be ommited, i.e. no =
and the right part when defining a directive.
For the above example (Authorization.Token
), there are three directives:
- d1:
query=access_token,token
- d2:
header=x-api-token
- d3:
required
Let's dissect d1, the name of the directive is query
, argv is access_token,token
.
A Directive Executor is an algorithm with runtime context. It is responsible for executing a concrete directive.
For better understanding, we can think of a Directive Executor as a function in a programming language, and a Directive as a concrete function call.
Directive | Executor | Directive Execution |
---|---|---|
query | query=access_token,token | query(["access_token", "token"]) |
header | header=x-api-token,Authorization | header(["x-api-token", "Authorization"]) |
required | required | required([]) |
Let's take a look at the following snippet to see how ggicci/goenv is implemented by only a little effort with the help of owl:
func exeEnvReader(rtm *owl.DirectiveRuntime) error {
if len(rtm.Directive.Argv) == 0 {
return nil
}
if value, ok := os.LookupEnv(rtm.Directive.Argv[0]); ok {
rtm.Value.Elem().SetString(value)
}
return nil
}
func init() {
owl.RegisterDirectiveExecutor("env", owl.DirectiveExecutorFunc(exeEnvReader))
}
Now, you are able to populate a struct instance by running the following code:
type EnvConfig struct {
Workspace string `owl:"env=OWL_HOME"`
User string `owl:"env=OWL_USER"`
Debug string `owl:"env=OWL_DEBUG"`
}
resolver, err := owl.New(EnvConfig{})
config, err := resolver.Resolve()
// Now, config.Workspace has the value of $OWL_HOME, ...