go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 654c9b4e62528a4415010b3a44b7bb09ee6b0eee
parent f034631227cefb6e5dd879eb9ab1744af7a78dbe
Author: lash <dev@holbrook.no>
Date:   Tue,  2 May 2023 09:05:09 +0100

Copy working example description to readme

Diffstat:
MCHANGELOG | 3+++
MREADME.md | 271++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mdoc/build/index.html | 3++-
Mdoc/texinfo/sim.texi | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mengine/engine.go | 1+
Mexamples/intro/bar | 3++-
Mexamples/intro/bar.vis | 5+++++
Mexamples/intro/main.go | 23++++++++++++++---------
Mrender/menu.go | 2+-
Mrender/page.go | 3+++
Mrender/size.go | 5+++++
11 files changed, 348 insertions(+), 57 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,3 +1,6 @@ +- 0.1.15 + * Add comprehensive documentation with texinfo. + * Prevent cursor leak in sizer. - 0.1.14 * Add language support for menus. * Support for multi-page menus (only works for template renders without a sink). diff --git a/README.md b/README.md @@ -1,52 +1,261 @@ # vise: A Constrained Size Output Virtual Machine -An attempt at defining a small VM to handle menu interaction for size-constrained clients. +Consider the following interaction: -Original motivation for this project was to create a simple templating renderer for USSD clients, enhanced with a pluggable and agnostic interface for external data-retrieval. +``` +This is the root page +You have visited 1 time. +0:foo +1:bar -## Features +$ 1 +Please visit foo first. +Any input to return. -### Implemented +$ x +This is the root page. +You have visited 2 times. +0:foo +1:bar -* Define and enforce max output size for every individual output. -* Allow one single data entry to fill remaining available size capacity. -* An assembly-like mini-language to define: - - external code execution. - - input validation and routing. - - menu definitions. - - flow control. - - exception handling. -* templated output from results of external code execution. -* generate and navigate pages where data symbol contents are too long to display on a single page. -* pluggable function design for handling external code execution calls. -* Dedicated error string to prepend to template (e.g. on catch) +$ 0 +Welcome to page foo. +Please write seomthing. +$ blah blah blah +This is the root page. +You have visited 3 times. +0:foo +1:bar -### Pending +$ 1 +Thanks for visiting foo and bar. +You have written: +blah blah blah +``` -* Node Walking Audit Tool (NWAT) to ensure all nodes produce output within constraints. -* Input generator engine for the NWAT. -* State error flag debugger representation, builtin as well as user-defined. -* Stepwise debug view on log/stderr of state mutations. -* Toolset to assist bootstrapping/recovering (failed) state from spec. +## Components +This simple interface above involves four different menu nodes. -### Possibly useful +In order to engineer these using vise, three types of components are involved: -* Breakpoints. -* Key/value database reference example. +* An assembly-like menu handling script. +* A display template. +* External code handlers for the counter and the "something" input. -## Documentation +## Nodes -Please refer to `doc/build/index.html`. +* `root` - The first page +* `foo` - The "foo" page +* `bar` - The "bar" page after "foo" has been visited +* `ouch` - The "bar" page before "foo" has been visited -Docs can be rebuilt using `make -B doc`. -Documentation sources are found in `doc/texinfo/` +## Templates +Each page has a template that may or may not contain dynamic elements. +In this example the root and bar nodes contains dynamic content. -## Examples +### root -Build examples with `make -B examples` +``` +This is the root page +You have visited {{.count}}. +``` + + +### foo + +``` +Welcome to page foo. +Please write something. +``` + + +### bar + +``` +Thanks for visiting foo and bar. +You have written: +{{.something}} +``` + + +### ouch + +``` +Please visit foo first. +Any input to return. +``` + + +## Scripts + +The scripts are responsible for defining menus, handling navigation flow control, and triggering external code handlers. + +### root + +``` +LOAD count 8 # trigger external code handler "count" +LOAD something 0 # trigger external code handler "something" +RELOAD count # explicitly trigger "count" every time this code is executed. +MAP count # make the result from "count" available to the template renderer +MOUT foo 0 # menu item +MOUT bar 1 # menu item +HALT # render template and wait for input +INCMP foo 0 # match menu selection 0, move to node "foo" on match +INCMP bar 1 # match menu selection 1, move to node "bar" on match +``` + +### foo + +``` +HALT # render template and wait for input +RELOAD something # pass input to the "something" external code handler. + # The input will be appended to the stored value. + # The "HAVESOMETHING" flag (8) will be set. +MOVE _ # move up one level +``` + + +### bar + +``` +CATCH ouch 8 0 # if the "HAVESOMETHING" (8) flag has NOT (0) been set, move to "ouch" +MNEXT next 11 # menu choice to display for advancing one page +MPREV back 22 # menu choice to display for going back to the previous page +MAP something # make the result from "something" available to the template renderer +HALT # render template and wait for input +INCMP > 11 # handle the "next" menu choice +INCMP < 22 # handle to "back" menu choice +INCMP ^ * # move to the root node on any input +``` + + +### ouch + +``` +HALT # render template and wait for input +INCMP ^ * # move to the root node on any input +``` + +## External code handlers + +The script code contains `LOAD` instructions for two different methods. + +``` +import ( + "context" + "fmt" + "path" + "strings" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/state" + "git.defalsify.org/vise.git/resource" +) + +const ( + USERFLAG_HAVESOMETHING = iota + state.FLAG_USERSTART +) + +var ( + baseDir = testdataloader.GetBasePath() + scriptDir = path.Join(baseDir, "examples", "intro") +) + +type introResource struct { + *resource.FsResource + c int64 + v []string +} + +func newintroResource() introResource { + fs := resource.NewFsResource(scriptDir) + return introResource{fs, 0, []string{}} +} + +// increment counter. +// return a string representing the current value of the counter. +func(c *introResource) count(ctx context.Context, sym string, input []byte) (resource.Result, error) { + s := "%v time" + if c.c != 1 { + s += "s" + } + r := resource.Result{ + Content: fmt.Sprintf(s, c.c), + } + c.c += 1 + return r, nil +} + +// if input is suppled, append it to the stored string vector and set the HAVESOMETHING flag. +// return the stored string vector value, one string per line. +func(c *introResource) something(ctx context.Context, sym string, input []byte) (resource.Result, error) { + c.v = append(c.v, string(input)) + r := resource.Result{ + Content: strings.Join(c.v, "\n"), + } + if len(input) > 0 { + r.FlagSet = []uint32{USERFLAG_HAVESOMETHING} + } + return r, nil +} +``` + +## Handling long values + +In the above example, the more times the `foo` page is supplied with a value, the longer the vector of values that need to be displayed by the `bar` page will be. + +A core feature of `vise` is to magically create browseable pages from these values from a pre-defined maximum output capacity for each page. + +Consider the case where the contents of the `something` symbol has become: + +``` +foo bar +baz bazbaz +inky pinky +blinky +clyde +``` + +Given a size constaint of 90 characters, the display will be split into two pages: + +``` +Thanks for visiting foo and bar. +You have written: +foo bar +baz bazbaz +up:any +11:next +``` + +``` +Thanks for visiting foo and bar. +You have written: +inky pinky +blinky +clyde +up:any +22:back +``` + + +## Working example + +In the source code repository, a full working example of this menu can be found in `examples/intro`. + +To run it: + +``` +make -B intro +go run ./examples/intro +``` + +Use `go run -tags logtrace ...` to peek at what is going on under the hood. + +To play the "Handling long values" case above, limit the output size by adding `-s 90`. diff --git a/doc/build/index.html b/doc/build/index.html @@ -65,7 +65,8 @@ Next: <a href="overview.html" accesskey="n" rel="next">Overview</a> &nbsp; [<a h <li><a id="toc-ouch-1" href="sim.html#ouch-1">2.2.4 ouch</a></li> </ul></li> <li><a id="toc-External-code-handlers" href="sim.html#External-code-handlers">2.3 External code handlers</a></li> - <li><a id="toc-Working-example" href="sim.html#Working-example">2.4 Working example</a></li> + <li><a id="toc-Handling-long-values" href="sim.html#Handling-long-values">2.4 Handling long values</a></li> + <li><a id="toc-Working-example" href="sim.html#Working-example">2.5 Working example</a></li> </ul></li> <li><a id="toc-Nomenclature" href="nomenclature.html">3 Nomenclature</a> <ul class="toc-numbered-mark"> diff --git a/doc/texinfo/sim.texi b/doc/texinfo/sim.texi @@ -32,7 +32,8 @@ You have visited 3 times. $ 1 Thanks for visiting foo and bar. -You wrote "blah blah blah" in foo. +You have written: +blah blah blah @end example The simple interface above involves four different menu nodes. @@ -118,7 +119,9 @@ INCMP bar 1 # match menu selection 1, move to node "bar" on match @verbatim HALT # render template and wait for input -RELOAD something # pass input to the "something" external code handler. The "HAVESOMETHING" flag (8) will be set +RELOAD something # pass input to the "something" external code handler. + # The input will be appended to the stored value. + # The "HAVESOMETHING" flag (8) will be set. MOVE _ # move up one level @end verbatim @@ -126,10 +129,14 @@ MOVE _ # move up one level @subsection bar @verbatim -CATCH ouch 8 0 # if the "HAVESOMETHING" (8) flag has NOT (0) been set, move to "ouch" -MAP something # make the result from "something" available to the template renderer -HALT # render template and wait for input -INCMP ^ * # move to the root node on any input +CATCH ouch 8 0 # if the "HAVESOMETHING" (8) flag has NOT (0) been set, move to "ouch" +MNEXT next 11 # menu choice to display for advancing one page +MPREV back 22 # menu choice to display for going back to the previous page +MAP something # make the result from "something" available to the template renderer +HALT # render template and wait for input +INCMP > 11 # handle the "next" menu choice +INCMP < 22 # handle to "back" menu choice +INCMP ^ * # move to the root node on any input @end verbatim @@ -147,6 +154,12 @@ The script code contains @code{LOAD} instructions for two different methods. @verbatim import ( + "context" + "fmt" + "path" + "strings" + + testdataloader "github.com/peteole/testdata-loader" "git.defalsify.org/vise.git/state" @@ -162,18 +175,20 @@ var ( scriptDir = path.Join(baseDir, "examples", "intro") ) -type Counter struct { +type introResource struct { *resource.FsResource c int64 - v string + v []string } -func newCounter() Counter { +func newintroResource() introResource { fs := resource.NewFsResource(scriptDir) - return Counter{fs, 0, ""} + return introResource{fs, 0, []string{}} } -func(c *Counter) count(ctx context.Context, sym string, input []byte) (resource.Result, error) { +// increment counter. +// return a string representing the current value of the counter. +func(c *introResource) count(ctx context.Context, sym string, input []byte) (resource.Result, error) { s := "%v time" if c.c != 1 { s += "s" @@ -185,10 +200,12 @@ func(c *Counter) count(ctx context.Context, sym string, input []byte) (resource. return r, nil } -func(c *Counter) something(ctx context.Context, sym string, input []byte) (resource.Result, error) { - c.v = string(input) +// if input is suppled, append it to the stored string vector and set the HAVESOMETHING flag. +// return the stored string vector value, one string per line. +func(c *introResource) something(ctx context.Context, sym string, input []byte) (resource.Result, error) { + c.v = append(c.v, string(input)) r := resource.Result{ - Content: c.v, + Content: strings.Join(c.v, "\n"), } if len(input) > 0 { r.FlagSet = []uint32{USERFLAG_HAVESOMETHING} @@ -198,6 +215,45 @@ func(c *Counter) something(ctx context.Context, sym string, input []byte) (resou @end verbatim +@anchor{long_values} +@section Handling long values + +In the above example, the more times the @code{foo} page is supplied with a value, the longer the vector of values that need to be displayed by the @code{bar} page will be. + +A core feature of @code{vise} is to magically create browseable pages from these values, from a pre-defined maximum output capacity for each page. + +Consider the case where the contents of the @code{something} symbol has become: + +@verbatim +foo bar +baz bazbaz +inky pinky +blinky +clyde +@end verbatim + +Given a size constaint of 90 characters, the display will be split into two pages: + +@verbatim +Thanks for visiting foo and bar. +You have written: +foo bar +baz bazbaz +up:any +11:next +@end verbatim + +@verbatim +Thanks for visiting foo and bar. +You have written: +inky pinky +blinky +clyde +up:any +22:back +@end verbatim + + @section Working example In the source code repository, a full working example of this menu can be found in @file{examples/intro}. @@ -210,3 +266,5 @@ go run ./examples/intro @end example Use @code{go run -tags logtrace ...} to peek at what is going on under the hood. + +To play the @ref{long_values, Long Values} case above, limit the output size by adding @code{-s 90}. diff --git a/engine/engine.go b/engine/engine.go @@ -205,6 +205,7 @@ func(en *Engine) WriteResult(ctx context.Context, w io.Writer) (int, error) { if en.st.Language != nil { ctx = context.WithValue(ctx, "Language", *en.st.Language) } + Logg.TraceCtxf(ctx, "render with state", "state", en.st) r, err := en.vm.Render(ctx) if err != nil { return 0, err diff --git a/examples/intro/bar b/examples/intro/bar @@ -1,2 +1,3 @@ Thanks for visiting foo and bar. -You wrote "{{.something}}" in foo. +You have written: +{{.something}} diff --git a/examples/intro/bar.vis b/examples/intro/bar.vis @@ -1,4 +1,9 @@ CATCH ouch 8 0 MAP something +MNEXT next 11 +MPREV back 22 +MOUT up any HALT +INCMP > 11 +INCMP < 22 INCMP ^ * diff --git a/examples/intro/main.go b/examples/intro/main.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path" + "strings" testdataloader "github.com/peteole/testdata-loader" @@ -24,18 +25,20 @@ var ( scriptDir = path.Join(baseDir, "examples", "intro") ) -type Counter struct { +type introResource struct { *resource.FsResource c int64 - v string + v []string } -func newCounter() Counter { +func newintroResource() introResource { fs := resource.NewFsResource(scriptDir) - return Counter{fs, 0, ""} + return introResource{fs, 0, []string{}} } -func(c *Counter) count(ctx context.Context, sym string, input []byte) (resource.Result, error) { +// increment counter. +// return a string representing the current value of the counter. +func(c *introResource) count(ctx context.Context, sym string, input []byte) (resource.Result, error) { s := "%v time" if c.c != 1 { s += "s" @@ -47,10 +50,12 @@ func(c *Counter) count(ctx context.Context, sym string, input []byte) (resource. return r, nil } -func(c *Counter) something(ctx context.Context, sym string, input []byte) (resource.Result, error) { - c.v = string(input) +// if input is suppled, append it to the stored string vector and set the HAVESOMETHING flag. +// return the stored string vector value, one string per line. +func(c *introResource) something(ctx context.Context, sym string, input []byte) (resource.Result, error) { + c.v = append(c.v, string(input)) r := resource.Result{ - Content: c.v, + Content: strings.Join(c.v, "\n"), } if len(input) > 0 { r.FlagSet = []uint32{USERFLAG_HAVESOMETHING} @@ -70,7 +75,7 @@ func main() { fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) st := state.NewState(3) - rs := newCounter() + rs := newintroResource() rs.AddLocalFunc("count", rs.count) rs.AddLocalFunc("something", rs.something) ca := cache.NewCache() diff --git a/render/menu.go b/render/menu.go @@ -54,7 +54,7 @@ type Menu struct { } func(m Menu) String() string { - return fmt.Sprintf("pagecount: %v sink: %v next: %v prev: %v", m.pageCount, m.sink, m.canNext, m.canPrevious) + return fmt.Sprintf("pagecount: %v menusink: %v next: %v prev: %v", m.pageCount, m.sink, m.canNext, m.canPrevious) } // NewMenu creates a new Menu with an explicit page count. diff --git a/render/page.go b/render/page.go @@ -206,6 +206,9 @@ func(pg *Page) Reset() { if pg.menu != nil { pg.menu.Reset() } + if pg.sizer != nil { + pg.sizer.Reset() + } } // extract sink values to separate array, and set the content of sink in values map to zero-length string. diff --git a/render/size.go b/render/size.go @@ -95,6 +95,7 @@ func(szr *Sizer) GetAt(values map[string]string, idx uint16) (map[string]string, } outValues := make(map[string]string) for k, v := range values { + Logg.Tracef("check values", "k", k, "v", v, "idx", idx, "cursors", szr.crsrs) if szr.sink == k { if idx >= uint16(len(szr.crsrs)) { return nil, fmt.Errorf("no more values in index") @@ -112,3 +113,7 @@ func(szr *Sizer) GetAt(values map[string]string, idx uint16) (map[string]string, } return outValues, nil } + +func(szr *Sizer) Reset() { + szr.crsrs = []uint32{} +}