diff --git a/docs/function-guide.md b/docs/function-guide.md new file mode 100644 index 00000000..5337b102 --- /dev/null +++ b/docs/function-guide.md @@ -0,0 +1,318 @@ +# Function guide + +## Overview + +The `mgmt` tool has built-in functions which add useful, reactive functionality +to the language. This guide describes the different function API's that are +available. It is meant to instruct developers on how to write new functions. +Since `mgmt` and the core functions are written in golang, some prior golang +knowledge is assumed. + +## Theory + +Functions in `mgmt` are similar to functions in other languages, however they +also have a [reactive](https://en.wikipedia.org/wiki/Functional_reactive_programming) +component. Our functions can produce events over time, and there are different +ways to write functions. For some background on this design, please read the +[original article](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) +on the subject. + +## Native Functions + +Native functions are functions which are implemented in the mgmt language +itself. These are currently not available yet, but are coming soon. Stay tuned! + +## Simple Function API + +Most functions should be implemented using the simple function API. This API +allows you to implement simple, static, [pure](https://en.wikipedia.org/wiki/Pure_function) +functions that don't require you to write much boilerplate code. They will be +automatically re-evaluated as needed when their input values change. These will +all be automatically made available as helper functions within mgmt templates, +and are also available for use anywhere inside mgmt programs. + +You'll need some basic knowledge of using the [`types`](https://github.com/purpleidea/mgmt/tree/master/lang/types) +library which is included with mgmt. This library lets you interact with the +available types and values in the mgmt language. It is very easy to use, and +should be fairly intuitive. Most of what you'll need to know can be inferred +from looking at example code. + +To implement a function, you'll need to create a file in +[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/). +The function should be implemented as a `FuncValue` in our type system. It is +then registered with the engine during `init()`. An example explains it best: + +### Example + +```golang +package simple + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/types" +) + +// you must register your functions in init when the program starts up +func init() { + // Example function that squares an int and prints out answer as an str. + Register("talkingsquare", &types.FuncValue{ + T: types.NewType("func(a int) str"), // declare the signature + V: func(input []types.Value) (types.Value, error) { + i := input[0].Int() // get first arg as an int64 + // must return the above specified value + return &types.StrValue{ + V: fmt.Sprintf("%d^2 is %d", i, i * i), + }, nil // no serious errors occurred + }, + }) +} +``` + +This simple function accepts one `int` as input, and returns one `str`. +Functions can have zero or more inputs, and must have exactly one output. You +must be sure to use the `types` library correctly, since if you try and access +an input which should not exist (eg: `input[2]`, when there are only two +that are expected), then you will cause a panic. If you have declared that a +particular argument is an `int` but you try to read it with `.Bool()` you will +also cause a panic. Lastly, make sure that you return a value in the correct +type or you will also cause a panic! + +If anything goes wrong, you can return an error, however this will cause the +mgmt engine to shutdown. It should be seen as the equivalent to calling a +`panic()`, however it is safer because it brings the engine down cleanly. +Ideally, your functions should never need to error. You should never cause a +real `panic()`, since this could have negative consequences to the system. + +## Function API + +To implement a reactive function in `mgmt` it must satisfy the +[`Func`](https://github.com/purpleidea/mgmt/blob/master/lang/interfaces/func.go) +interface. Using the [Simple Function API](#simple-function-api) is preferable +if it meets your needs. Most functions will be able to use that API. If you +really need something more powerful, then you can use the regular function API. +What follows are each of the method signatures and a description of each. + +### Default + +```golang +Info() *interfaces.Info +``` + +This returns some information about the function. It is necessary so that the +compiler can type check the code correctly, and know what optimizations can be +performed. This is usually the first method which is called by the engine. + +#### Example + +```golang +func (obj *FooFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Sig: types.NewType("func(a int) str"), + } +} +``` + +### Init + +```golang +Init(init *interfaces.Init) error +``` + +This is called to initialize the function. If something goes wrong, it should +return an error. It is passed a struct that contains all the important +information and poiinters that it might need to work with throughout its +lifetime. As a result, it will need to save a copy to that pointer for future +use in the other methods. + +#### Example + +```golang +// Init runs some startup code for this function. +func (obj *FooFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) // shutdown signal + return nil +} +``` + +### Close + +```golang +Close() error +``` + +This is called to cleanup the function. It usually causes the stream to +shutdown. Even if `Stream()` decided to shutdown early, it might still get +called. It is usually called by the engine to tell the function to shutdown. + +#### Example + +```golang +// Close runs some shutdown code for this function and turns off the stream. +func (obj *FooFunc) Close() error { + close(obj.closeChan) // send a signal to tell the stream to close + return nil +} +``` + +### Stream + +```golang +Stream() error +``` + +`Stream` is where the real _work_ is done. This method is started by the +language function engine. It will run this function while simultaneously sending +it values on the `input` channel. It will only send a complete set of input +values. You should send a value to the output channel when you have decided that +one should be produced. Make sure to only use input values of the expected type +as declared in the `Info` struct, and send values of the similarly declared +appropriate return type. Failure to do so will may result in a panic and +sadness. + +#### Example + +```golang +// Stream returns the single value that was generated and then closes. +func (obj *FooFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + var result string + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + + ix := input.Struct()["a"].Int() + if ix < 0 { + return fmt.Errorf("we can't deal with negatives") + } + + result = fmt.Sprintf("the input is: %d", ix) + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- &types.StrValue{ + V: result, + }: + + case <-obj.closeChan: + return nil + } + } +} +``` + +As you can see, we read our inputs from the `input` channel, and write to the +`output` channel. Our code is careful to never block or deadlock, and can always +exit if a close signal is requested. It also cleans up after itself by closing +the `output` channel when it is done using it. This is done easily with `defer`. +If it notices that the `input` channel closes, then it knows that no more input +values are coming and it can consider shutting down early. + +## Further considerations + +There is some additional information that any function author will need to know. +Each issue is listed separately below! + +### Function struct + +Each function will implement methods as pointer receivers on a function struct. +The naming convention for resources is that they end with a `Func` suffix. + +#### Example + +```golang +type FooFunc struct { + init *interfaces.Init + + // this space can be used if needed + + closeChan chan struct{} // shutdown signal +} +``` + +### Function registration + +All functions must be registered with the engine so that they can be found. This +also ensures they can be encoded and decoded. Make sure to include the following +code snippet for this to work. + +```golang +func init() { // special golang method that runs once + funcs.Register("foo", func() interfaces.Func { return &FooFunc{} }) +} +``` + +### Composite functions + +Composite functions are functions which import one or more existing functions. +This is useful to prevent code duplication in higher level function scenarios. +Unfortunately no further documentation about this subject has been written. To +expand this section, please send a patch! Please contact us if you'd like to +work on a function that uses this feature, or to add it to an existing one! +We don't expect this functionality to be particularly useful or common, as it's +probably easier and preferable to simply import common golang library code into +multiple different functions instead. + +## Polymorphic Function API + +The polymorphic function API is an API that lets you implement functions which +do not necessarily have a single static function signature. After compile time, +all functions must have a static function signature. We also know that there +might be different ways you would want to call `printf`, such as: +`printf("the %s is %d", "answer", 42)` or `printf("3 * 2 = %d", 3 * 2)`. Since +you couldn't implement the infinite number of possible signatures, this API lets +you write code which can be coerced into different forms. This makes +implementing what would appear to be generic or polymorphic, instead something +that is actually static and that still has the static type safety properties +that were guaranteed by the mgmt language. + +Since this is an advanced topic, it is not described in full at this time. For +more information please have a look at the source code comments, some of the +existing implementations, and ask around in the community. + +## Frequently asked questions + +(Send your questions as a patch to this FAQ! I'll review it, merge it, and +respond by commit with the answer.) + +### Can I use global variables? + +Probably not. You must assume that multiple copies of your function may be used +at the same time. If they require a global variable, it's likely this won't +work. Instead it's probably better to use a struct local variable if you need to +store some state. + +There might be some rare instances where a global would be acceptable, but if +you need one of these, you're probably already an internals expert. If you think +they need to lock or synchronize so as to not overwhelm an external resource, +then you have to be especially careful not to cause deadlocking the mgmt engine. + +### Can I write functions in a different language? + +Currently `golang` is the only supported language for built-in functions. We +might consider allowing external functions to be imported in the future. This +will likely require a language that can expose a C-like API, such as `python` or +`ruby`. Custom `golang` functions are already possible when using mgmt as a lib. + +### What new functions need writing? + +There are still many ideas for new functions that haven't been written yet. If +you'd like to contribute one, please contact us and tell us about your idea! + +### Where can I find more information about mgmt? + +Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md). + +## Suggestions + +If you have any ideas for API changes or other improvements to function writing, +please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in +order to get it right! diff --git a/docs/language-guide.md b/docs/language-guide.md index 4656f640..fdd7d309 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -212,6 +212,8 @@ Your function must have a specific type. For example, a simple math function might have a signature of `func(x int, x int) int`. As you can see, all the types are known _before_ compile time. +A separate discussion on this matter can be found in the [function guide](function-guide.md). + What follows are each of the method signatures and a description of each. Failure to implement the API correctly can cause the function graph engine to block, or the program to panic. @@ -279,7 +281,7 @@ one value must be produced. #### Example ```golang Please see the example functions in -[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/). ``` ### Stream @@ -298,7 +300,7 @@ whether or not you close the `Output` channel. #### Example ```golang Please see the example functions in -[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/). ``` ### Close @@ -312,7 +314,7 @@ return. #### Example ```golang Please see the example functions in -[lang/funcs/public/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/public/). +[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/). ``` ### Polymorphic Function API diff --git a/examples/lang/datetime1.mcl b/examples/lang/datetime1.mcl index e898ed97..e7654684 100644 --- a/examples/lang/datetime1.mcl +++ b/examples/lang/datetime1.mcl @@ -1,4 +1,4 @@ $d = datetime() file "/tmp/mgmt/datetime" { - content => template("Hello! It is now: {{ datetimeprint . }}\n", $d), + content => template("Hello! It is now: {{ datetime_print . }}\n", $d), } diff --git a/examples/lang/datetime2.mcl b/examples/lang/datetime2.mcl index 9cd495ba..16a7e2d1 100644 --- a/examples/lang/datetime2.mcl +++ b/examples/lang/datetime2.mcl @@ -9,6 +9,6 @@ $theload = structlookup(load(), "x1") if 5 > 3 { file "/tmp/mgmt/datetime" { - content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetimeprint .year }}\n\nload average: {{ .load }}\n", $tmplvalues), + content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n", $tmplvalues), } } diff --git a/examples/lang/datetime3.mcl b/examples/lang/datetime3.mcl index 03ad2474..a5591bdc 100644 --- a/examples/lang/datetime3.mcl +++ b/examples/lang/datetime3.mcl @@ -10,5 +10,5 @@ $theload = structlookup(load(), "x1") $vumeter = vumeter("====", 10, 0.9) file "/tmp/mgmt/datetime" { - content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetimeprint .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues), + content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues), } diff --git a/examples/lang/history1.mcl b/examples/lang/history1.mcl index 90df8b76..541f328b 100644 --- a/examples/lang/history1.mcl +++ b/examples/lang/history1.mcl @@ -3,5 +3,5 @@ $dt = datetime() $hystvalues = {"ix0" => $dt, "ix1" => $dt{1}, "ix2" => $dt{2}, "ix3" => $dt{3},} file "/tmp/mgmt/history" { - content => template("Index(0) {{.ix0}}: {{ datetimeprint .ix0 }}\nIndex(1) {{.ix1}}: {{ datetimeprint .ix1 }}\nIndex(2) {{.ix2}}: {{ datetimeprint .ix2 }}\nIndex(3) {{.ix3}}: {{ datetimeprint .ix3 }}\n", $hystvalues), + content => template("Index(0) {{.ix0}}: {{ datetime_print .ix0 }}\nIndex(1) {{.ix1}}: {{ datetime_print .ix1 }}\nIndex(2) {{.ix2}}: {{ datetime_print .ix2 }}\nIndex(3) {{.ix3}}: {{ datetime_print .ix3 }}\n", $hystvalues), } diff --git a/examples/lang/template0.mcl b/examples/lang/template0.mcl new file mode 100644 index 00000000..d4a1b09f --- /dev/null +++ b/examples/lang/template0.mcl @@ -0,0 +1,10 @@ +$answer = 42 +$s = int2str($answer) + +print "print1" { + msg => printf("an str is: %s", $s), +} + +print "print2" { + msg => template("an str is: {{ int2str . }}", $answer), +} diff --git a/lang/funcs/core/template_polyfunc.go b/lang/funcs/core/template_polyfunc.go index 254c1889..c1ad2bf1 100644 --- a/lang/funcs/core/template_polyfunc.go +++ b/lang/funcs/core/template_polyfunc.go @@ -20,16 +20,23 @@ package core // TODO: should this be in its own individual package? import ( "bytes" "fmt" + "reflect" "text/template" - "time" "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" errwrap "github.com/pkg/errors" ) +var ( + // errorType represents a reflection type of error as seen in: + // https://github.com/golang/go/blob/ec62ee7f6d3839fe69aeae538dadc1c9dc3bf020/src/text/template/exec.go#L612 + errorType = reflect.TypeOf((*error)(nil)).Elem() +) + func init() { funcs.Register("template", func() interfaces.Func { return &TemplateFunc{} }) } @@ -42,7 +49,8 @@ const TemplateName = "template" // to it. It examines the type of the second argument (the input data vars) at // compile time and then determines the static functions signature by including // that in the overall signature. -// XXX: do we need to add events if any of the internal functions change over time? +// TODO: We *might* need to add events for internal function changes over time, +// but only if they are not pure. We currently only use simple, pure functions. type TemplateFunc struct { Type *types.Type // type of vars @@ -159,13 +167,40 @@ func (obj *TemplateFunc) Init(init *interfaces.Init) error { // run runs a template and returns the result. func (obj *TemplateFunc) run(templateText string, vars types.Value) (string, error) { + // see: https://golang.org/pkg/text/template/#FuncMap for more info + // note: we can override any other functions by adding them here... funcMap := map[string]interface{}{ - // XXX: can these functions come from normal funcValue things - // that we build for the interfaces.Func part? - // TODO: add a bunch of stdlib-like stuff here... - "datetimeprint": func(epochDelta int64) string { // TODO: rename - return time.Unix(epochDelta, 0).String() - }, + //"test1": func(in interface{}) (interface{}, error) { // ok + // return fmt.Sprintf("got(%T): %+v", in, in), nil + //}, + //"test2": func(in interface{}) interface{} { // NOT ok + // panic("panic") // a panic here brings down everything! + //}, + //"test3": func(foo int64) (string, error) { // ok, but errors + // return "", fmt.Errorf("i am an error") + //}, + //"test4": func(in1, in2 reflect.Value) (reflect.Value, error) { // ok + // s := fmt.Sprintf("got: %+v and: %+v", in1, in2) + // return reflect.ValueOf(s), nil + //}, + } + + // FIXME: should we do this once in init() instead, or in the Register + // function in the simple package? + // TODO: loop through this map in a sorted, deterministic order + for name, fn := range simple.RegisteredFuncs { + if _, exists := funcMap[name]; exists { + obj.init.Logf("warning, existing function named: `%s` exists", name) + continue + } + + // When template execution invokes a function with an argument + // list, that list must be assignable to the function's + // parameter types. Functions meant to apply to arguments of + // arbitrary type can use parameters of type interface{} or of + // type reflect.Value. + f := wrap(name, fn) // wrap it so that it meets API expectations + funcMap[name] = f // add it } var err error @@ -288,3 +323,70 @@ func (obj *TemplateFunc) Close() error { close(obj.closeChan) return nil } + +// wrap builds a function in the format expected by the template engine, and +// returns it as an interface{}. It does so by wrapping our type system and +// function API with what is expected from the reflection API. It returns a +// version that includes the optional second error return value so that our +// functions can return errors without causing a panic. +func wrap(name string, fn *types.FuncValue) interface{} { + if fn.T.Map == nil { + panic("malformed func type") + } + if len(fn.T.Map) != len(fn.T.Ord) { + panic("malformed func length") + } + in := []reflect.Type{} + for _, k := range fn.T.Ord { + t, ok := fn.T.Map[k] + if !ok { + panic("malformed func order") + } + if t == nil { + panic("malformed func arg") + } + + in = append(in, t.Reflect()) + } + out := []reflect.Type{fn.T.Out.Reflect(), errorType} + var variadic = false // currently not supported in our function value + typ := reflect.FuncOf(in, out, variadic) + + // wrap our function with the translation that is necessary + f := func(args []reflect.Value) (results []reflect.Value) { // build + innerArgs := []types.Value{} + zeroValue := reflect.Zero(fn.T.Out.Reflect()) // zero value of return type + for _, x := range args { + v, err := types.ValueOf(x) // reflect.Value -> Value + if err != nil { + r := reflect.ValueOf(errwrap.Wrapf(err, "function `%s` errored", name)) + if !r.Type().ConvertibleTo(errorType) { // for fun! + r = reflect.ValueOf(fmt.Errorf("function `%s` errored: %+v", name, err)) + } + e := r.Convert(errorType) // must be seen as an `error` + return []reflect.Value{zeroValue, e} + } + innerArgs = append(innerArgs, v) + } + + result, err := fn.Call(innerArgs) // call it + if err != nil { // function errored :( + // errwrap is a better way to report errors, if allowed! + r := reflect.ValueOf(errwrap.Wrapf(err, "function `%s` errored", name)) + if !r.Type().ConvertibleTo(errorType) { // for fun! + r = reflect.ValueOf(fmt.Errorf("function `%s` errored: %+v", name, err)) + } + e := r.Convert(errorType) // must be seen as an `error` + return []reflect.Value{zeroValue, e} + } else if result == nil { // someone wrote a bad function + r := reflect.ValueOf(fmt.Errorf("function `%s` returned nil", name)) + e := r.Convert(errorType) // must be seen as an `error` + return []reflect.Value{zeroValue, e} + } + + nilError := reflect.Zero(errorType) + return []reflect.Value{reflect.ValueOf(result.Value()), nilError} + } + val := reflect.MakeFunc(typ, f) + return val.Interface() +} diff --git a/lang/funcs/simple/datetime_print_func.go b/lang/funcs/simple/datetime_print_func.go new file mode 100644 index 00000000..9baf0ab3 --- /dev/null +++ b/lang/funcs/simple/datetime_print_func.go @@ -0,0 +1,42 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simple // TODO: should this be in its own individual package? + +import ( + "fmt" + "time" + + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + // TODO: should we support namespacing these, eg: datetime.print ? + // FIXME: consider renaming this to printf, and add in a format string? + Register("datetime_print", &types.FuncValue{ + T: types.NewType("func(a int) str"), + V: func(input []types.Value) (types.Value, error) { + epochDelta := input[0].Int() + if epochDelta < 0 { + return nil, fmt.Errorf("epoch delta must be positive") + } + return &types.StrValue{ + V: time.Unix(epochDelta, 0).String(), + }, nil + }, + }) +} diff --git a/lang/funcs/simple/example_errorbool_func.go b/lang/funcs/simple/example_errorbool_func.go new file mode 100644 index 00000000..b3e4905e --- /dev/null +++ b/lang/funcs/simple/example_errorbool_func.go @@ -0,0 +1,39 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simple // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + // TODO: should we support namespacing these, eg: example.errorbool ? + Register("example_errorbool", &types.FuncValue{ + T: types.NewType("func(a bool) str"), + V: func(input []types.Value) (types.Value, error) { + if input[0].Bool() { + return nil, fmt.Errorf("we errored on request") + } + return &types.StrValue{ + V: "set input to true to generate an error", + }, nil + }, + }) +} diff --git a/lang/funcs/simple/int2str_func.go b/lang/funcs/simple/int2str_func.go new file mode 100644 index 00000000..c7550f01 --- /dev/null +++ b/lang/funcs/simple/int2str_func.go @@ -0,0 +1,35 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simple // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + Register("int2str", &types.FuncValue{ + T: types.NewType("func(a int) str"), + V: func(input []types.Value) (types.Value, error) { + return &types.StrValue{ + V: fmt.Sprintf("%d", input[0].Int()), + }, nil + }, + }) +} diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go new file mode 100644 index 00000000..4dc63a7b --- /dev/null +++ b/lang/funcs/simple/simple.go @@ -0,0 +1,135 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simple // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +// RegisteredFuncs maps a function name to the corresponding static, pure func. +var RegisteredFuncs = make(map[string]*types.FuncValue) // must initialize + +// Register registers a simple, static, pure function. It is easier to use that +// the raw function API, but also limits you to simple, static, pure functions. +func Register(name string, fn *types.FuncValue) { + if _, exists := RegisteredFuncs[name]; exists { + panic(fmt.Sprintf("a simple func named %s is already registered", name)) + } + RegisteredFuncs[name] = fn // store a copy for ourselves + + // register a copy in the main function database + funcs.Register(name, func() interfaces.Func { return &simpleFunc{Fn: fn} }) +} + +// simpleFunc is a scaffolding function struct which fulfills the boiler-plate +// for the function API, but that can run a very simple, static, pure function. +type simpleFunc struct { + Fn *types.FuncValue + + init *interfaces.Init + last types.Value // last value received to use for diff + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *simpleFunc) Validate() error { + if obj.Fn == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + return nil +} + +// Info returns some static info about itself. +func (obj *simpleFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: should this be something we specify here? + Sig: obj.Fn.Type(), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this function. +func (obj *simpleFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *simpleFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + return nil // can't output any more + } + //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { + // return errwrap.Wrapf(err, "wrong function input") + //} + + if obj.last != nil && input.Cmp(obj.last) == nil { + continue // value didn't change, skip it + } + obj.last = input // store for next + + values := []types.Value{} + for _, name := range obj.Fn.Type().Ord { + x := input.Struct()[name] + values = append(values, x) + } + + result, err := obj.Fn.Call(values) // (Value, error) + if err != nil { + return errwrap.Wrapf(err, "simple function errored") + } + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + // pass + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *simpleFunc) Close() error { + close(obj.closeChan) + return nil +} diff --git a/lang/lang.go b/lang/lang.go index f9e1fa1c..c055c975 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -26,6 +26,7 @@ import ( "github.com/purpleidea/mgmt/lang/funcs" _ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // import so the facts register + _ "github.com/purpleidea/mgmt/lang/funcs/simple" // import so the funcs register "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" diff --git a/lang/types/value.go b/lang/types/value.go index 8d814413..61197417 100644 --- a/lang/types/value.go +++ b/lang/types/value.go @@ -47,8 +47,154 @@ type Value interface { } // ValueOf takes a reflect.Value and returns an equivalent Value. -func ValueOf(value reflect.Value) Value { - panic("not implemented") // XXX: not implemented +func ValueOf(v reflect.Value) (Value, error) { + value := v + typ := value.Type() + kind := typ.Kind() + for kind == reflect.Ptr { + typ = typ.Elem() // un-nest one pointer + kind = typ.Kind() + + // un-nest value from pointer + value = value.Elem() // XXX: is this correct? + } + + switch kind { // match on destination field kind + case reflect.Bool: + return &BoolValue{V: value.Bool()}, nil + + case reflect.String: + return &StrValue{V: value.String()}, nil + + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + return &IntValue{V: value.Int()}, nil + + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: + return &IntValue{V: int64(value.Uint())}, nil + + case reflect.Float64, reflect.Float32: + return &FloatValue{V: value.Float()}, nil + + case reflect.Array, reflect.Slice: + values := []Value{} + for i := 0; i < value.Len(); i++ { + x := value.Index(i) + v, err := ValueOf(x) // recurse + if err != nil { + return nil, err + } + values = append(values, v) + } + + t, err := TypeOf(value.Type().Elem()) // type of contents + if err != nil { + return nil, errwrap.Wrapf(err, "can't determine type of %+v", value) + } + + return &ListValue{ + T: NewType(fmt.Sprintf("[]%s", t.String())), + V: values, + }, nil + + case reflect.Map: + m := make(map[Value]Value) + + // loop through the list of map keys in undefined order + for _, mk := range value.MapKeys() { + mv := value.MapIndex(mk) + + k, err := ValueOf(mk) // recurse + if err != nil { + return nil, err + } + v, err := ValueOf(mv) // recurse + if err != nil { + return nil, err + } + + m[k] = v + } + + kt, err := TypeOf(value.Type().Key()) // type of key + if err != nil { + return nil, errwrap.Wrapf(err, "can't determine key type of %+v", value) + } + vt, err := TypeOf(value.Type().Elem()) // type of value + if err != nil { + return nil, errwrap.Wrapf(err, "can't determine value type of %+v", value) + } + + return &MapValue{ + T: NewType(fmt.Sprintf("{%s: %s}", kt.String(), vt.String())), + V: m, + }, nil + + case reflect.Struct: + // TODO: we could take this simpler "get the full type" approach + // for all the values, but I think that building them up when + // possible for the other cases is a more robust approach! + t, err := TypeOf(value.Type()) + if err != nil { + return nil, errwrap.Wrapf(err, "can't determine type of %+v", value) + } + l := value.Len() // number of struct fields according to value + + if l != len(t.Ord) { + // programming error? + return nil, fmt.Errorf("incompatible number of fields") + } + + values := make(map[string]Value) + for i := 0; i < l; i++ { + x := value.Field(i) + v, err := ValueOf(x) // recurse + if err != nil { + return nil, err + } + name := t.Ord[i] // how else can we get the field name? + values[name] = v + } + + return &StructValue{ + T: t, + V: values, + }, nil + + case reflect.Func: + t, err := TypeOf(value.Type()) + if err != nil { + return nil, errwrap.Wrapf(err, "can't determine type of %+v", value) + } + if t.Out == nil { + return nil, fmt.Errorf("cannot only represent functions with one output value") + } + + f := func(args []Value) (Value, error) { + in := []reflect.Value{} + for _, x := range args { + // TODO: should we build this method instead? + //v := x.Reflect() // types.Value -> reflect.Value + v := reflect.ValueOf(x.Value()) + in = append(in, v) + } + + // FIXME: can we trap panic's ? + out := value.Call(in) // []reflect.Value + if len(out) != 1 { // TODO: panic, b/c already checked in TypeOf? + return nil, fmt.Errorf("cannot only represent functions with one output value") + } + + return ValueOf(out[0]) // recurse + } + + return &FuncValue{ + T: t, + V: f, + }, nil + + default: + return nil, fmt.Errorf("unable to represent value of %+v", v) + } } // ValueSlice is a linear list of values. It is used for sorting purposes. @@ -800,7 +946,10 @@ func (obj *FuncValue) Value() interface{} { fn := func(args []reflect.Value) (results []reflect.Value) { // build innerArgs := []Value{} for _, x := range args { - v := ValueOf(x) // reflect.Value -> Value + v, err := ValueOf(x) // reflect.Value -> Value + if err != nil { + panic(fmt.Sprintf("can't determine value of %+v", x)) + } innerArgs = append(innerArgs, v) } result, err := obj.V(innerArgs) // call it @@ -836,18 +985,23 @@ func (obj *FuncValue) Call(args []Value) (Value, error) { // cmp input args type to obj.T length := len(obj.T.Ord) if length != len(args) { - panic(fmt.Sprintf("arg length of %d does not match expected of %d", len(args), length)) + return nil, fmt.Errorf("arg length of %d does not match expected of %d", len(args), length) } for i := 0; i < length; i++ { if err := args[i].Type().Cmp(obj.T.Map[obj.T.Ord[i]]); err != nil { - panic(fmt.Sprintf("cannot cmp input types: %+v", err)) + return nil, errwrap.Wrapf(err, "cannot cmp input types") } } result, err := obj.V(args) // call it - + if result == nil { + if err == nil { + return nil, fmt.Errorf("function returned nil result") + } + return nil, errwrap.Wrapf(err, "function returned nil result during error") + } if err := result.Type().Cmp(obj.T.Out); err != nil { - panic(fmt.Sprintf("cannot cmp return types: %+v", err)) + return nil, errwrap.Wrapf(err, "cannot cmp return types") } return result, err diff --git a/lang/types/value_test.go b/lang/types/value_test.go index f115b9af..78d006cf 100644 --- a/lang/types/value_test.go +++ b/lang/types/value_test.go @@ -586,3 +586,7 @@ func TestStruct2(t *testing.T) { //t.Errorf("val2: %s", val2) } } + +func TestValueOf0(t *testing.T) { + // TODO: implement testing of the ValueOf function +}