WIP Factor page, sizer, menu to render package, cachemap to page

This commit is contained in:
lash 2023-04-08 23:35:13 +01:00
parent 6a1611a0c8
commit 7e01f2725d
Signed by untrusted user who does not match committer: lash
GPG Key ID: 21D2E7BB88C2A746
14 changed files with 606 additions and 614 deletions

131
go/cache/cache.go vendored
View File

@ -9,11 +9,10 @@ type Cache struct {
CacheSize uint32 // Total allowed cumulative size of values (not code) in cache CacheSize uint32 // Total allowed cumulative size of values (not code) in cache
CacheUseSize uint32 // Currently used bytes by all values (not code) in cache CacheUseSize uint32 // Currently used bytes by all values (not code) in cache
Cache []map[string]string // All loaded cache items Cache []map[string]string // All loaded cache items
CacheMap map[string]string // Mapped //CacheMap map[string]string // Mapped
menuSize uint16 // Max size of menu //outputSize uint32 // Max size of output
outputSize uint32 // Max size of output
sizes map[string]uint16 // Size limits for all loaded symbols. sizes map[string]uint16 // Size limits for all loaded symbols.
sink *string //sink *string
} }
// NewCache creates a new ready-to-use cache object // NewCache creates a new ready-to-use cache object
@ -22,7 +21,7 @@ func NewCache() *Cache {
Cache: []map[string]string{make(map[string]string)}, Cache: []map[string]string{make(map[string]string)},
sizes: make(map[string]uint16), sizes: make(map[string]uint16),
} }
ca.resetCurrent() //ca.resetCurrent()
return ca return ca
} }
@ -32,12 +31,6 @@ func(ca *Cache) WithCacheSize(cacheSize uint32) *Cache {
return ca return ca
} }
// WithCacheSize applies a cumulative cache size limitation for all cached items.
func(ca *Cache) WithOutputSize(outputSize uint32) *Cache {
ca.outputSize = outputSize
return ca
}
// Add adds a cache value under a cache symbol key. // Add adds a cache value under a cache symbol key.
// //
// Also stores the size limitation of for key for later updates. // Also stores the size limitation of for key for later updates.
@ -72,6 +65,15 @@ func(ca *Cache) Add(key string, value string, sizeLimit uint16) error {
return nil return nil
} }
// ReservedSize returns the maximum byte size available for the given symbol.
func(ca *Cache) ReservedSize(key string) (uint16, error) {
v, ok := ca.sizes[key]
if !ok {
return 0, fmt.Errorf("unknown symbol: %s", key)
}
return v, nil
}
// Update sets a new value for an existing key. // Update sets a new value for an existing key.
// //
// Uses the size limitation from when the key was added. // Uses the size limitation from when the key was added.
@ -95,9 +97,9 @@ func(ca *Cache) Update(key string, value string) error {
r := ca.Cache[checkFrame][key] r := ca.Cache[checkFrame][key]
l := uint32(len(r)) l := uint32(len(r))
ca.Cache[checkFrame][key] = "" ca.Cache[checkFrame][key] = ""
if ca.CacheMap[key] != "" { //if ca.CacheMap[key] != "" {
ca.CacheMap[key] = value // ca.CacheMap[key] = value
} //}
ca.CacheUseSize -= l ca.CacheUseSize -= l
sz := ca.checkCapacity(value) sz := ca.checkCapacity(value)
if sz == 0 { if sz == 0 {
@ -117,58 +119,6 @@ func(ca *Cache) Get() (map[string]string, error) {
return ca.Cache[len(ca.Cache)-1], nil return ca.Cache[len(ca.Cache)-1], nil
} }
func(ca *Cache) Sizes() (map[string]uint16, error) {
if len(ca.Cache) == 0 {
return nil, fmt.Errorf("get at top frame")
}
sizes := make(map[string]uint16)
var haveSink bool
for k, _ := range ca.CacheMap {
l, ok := ca.sizes[k]
if !ok {
panic(fmt.Sprintf("missing size for %v", k))
}
if l == 0 {
if haveSink {
panic(fmt.Sprintf("duplicate sink for %v", k))
}
haveSink = true
}
sizes[k] = l
}
return sizes, nil
}
// Map marks the given key for retrieval.
//
// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations.
//
// Only one symbol with no size limitation may be mapped at the current level.
func(ca *Cache) Map(key string) error {
m, err := ca.Get()
if err != nil {
return err
}
l := ca.sizes[key]
if l == 0 {
if ca.sink != nil {
return fmt.Errorf("sink already set to symbol '%v'", *ca.sink)
}
ca.sink = &key
}
ca.CacheMap[key] = m[key]
return nil
}
// Fails if key is not mapped.
func(ca *Cache) Val(key string) (string, error) {
r := ca.CacheMap[key]
if len(r) == 0 {
return "", fmt.Errorf("key %v not mapped", key)
}
return r, nil
}
// Reset flushes all state contents below the top level. // Reset flushes all state contents below the top level.
func(ca *Cache) Reset() { func(ca *Cache) Reset() {
if len(ca.Cache) == 0 { if len(ca.Cache) == 0 {
@ -179,36 +129,11 @@ func(ca *Cache) Reset() {
return return
} }
// Size returns size used by values and menu, and remaining size available
func(ca *Cache) Usage() (uint32, uint32) {
var l int
var c uint16
for k, v := range ca.CacheMap {
l += len(v)
c += ca.sizes[k]
}
r := uint32(l)
r += uint32(ca.menuSize)
return r, uint32(c)-r
}
// return 0-indexed frame number where key is defined. -1 if not defined
func(ca *Cache) frameOf(key string) int {
for i, m := range ca.Cache {
for k, _ := range m {
if k == key {
return i
}
}
}
return -1
}
// Push adds a new level to the cache. // Push adds a new level to the cache.
func (ca *Cache) Push() error { func (ca *Cache) Push() error {
m := make(map[string]string) m := make(map[string]string)
ca.Cache = append(ca.Cache, m) ca.Cache = append(ca.Cache, m)
ca.resetCurrent() //ca.resetCurrent()
return nil return nil
} }
@ -228,7 +153,7 @@ func (ca *Cache) Pop() error {
log.Printf("free frame %v key %v value size %v", l, k, sz) log.Printf("free frame %v key %v value size %v", l, k, sz)
} }
ca.Cache = ca.Cache[:l] ca.Cache = ca.Cache[:l]
ca.resetCurrent() //ca.resetCurrent()
return nil return nil
} }
@ -238,10 +163,10 @@ func(ca *Cache) Check(key string) bool {
} }
// flush relveant properties for level change // flush relveant properties for level change
func(ca *Cache) resetCurrent() { //func(ca *Cache) resetCurrent() {
ca.sink = nil // ca.sink = nil
ca.CacheMap = make(map[string]string) // ca.CacheMap = make(map[string]string)
} //}
// bytes that will be added to cache use size for string // bytes that will be added to cache use size for string
// returns 0 if capacity would be exceeded // returns 0 if capacity would be exceeded
@ -255,3 +180,15 @@ func(ca *Cache) checkCapacity(v string) uint32 {
} }
return sz return sz
} }
// return 0-indexed frame number where key is defined. -1 if not defined
func(ca *Cache) frameOf(key string) int {
for i, m := range ca.Cache {
for k, _ := range m {
if k == key {
return i
}
}
}
return -1
}

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
) )
func TestNewCache(t *testing.T) { func TestNewCache(t *testing.T) {
ca := NewCache() ca := NewCache()
if ca.CacheSize != 0 { if ca.CacheSize != 0 {
@ -34,7 +33,6 @@ func TestStateCacheUse(t *testing.T) {
} }
} }
func TestStateDownUp(t *testing.T) { func TestStateDownUp(t *testing.T) {
ca := NewCache() ca := NewCache()
err := ca.Push() err := ca.Push()
@ -104,83 +102,3 @@ func TestCacheLoadDup(t *testing.T) {
} }
} }
func TestCacheCurrentSize(t *testing.T) {
ca := NewCache()
err := ca.Push()
if err != nil {
t.Error(err)
}
err = ca.Add("foo", "inky", 0)
if err != nil {
t.Error(err)
}
err = ca.Push()
err = ca.Add("bar", "pinky", 10)
if err != nil {
t.Error(err)
}
err = ca.Map("bar")
if err != nil {
t.Error(err)
}
err = ca.Add("baz", "tinkywinkydipsylalapoo", 51)
if err != nil {
t.Error(err)
}
err = ca.Map("baz")
if err != nil {
t.Error(err)
}
l, c := ca.Usage()
if l != 27 {
t.Errorf("expected actual length 27, got %v", l)
}
if c != 34 {
t.Errorf("expected remaining length 34, got %v", c)
}
}
func TestStateMapSink(t *testing.T) {
ca := NewCache()
ca.Push()
err := ca.Add("foo", "bar", 0)
if err != nil {
t.Error(err)
}
ca.Push()
err = ca.Add("bar", "xyzzy", 6)
if err != nil {
t.Error(err)
}
err = ca.Add("baz", "bazbaz", 18)
if err != nil {
t.Error(err)
}
err = ca.Add("xyzzy", "plugh", 0)
if err != nil {
t.Error(err)
}
err = ca.Map("foo")
if err != nil {
t.Error(err)
}
err = ca.Map("xyzzy")
if err == nil {
t.Errorf("Expected fail on duplicate sink")
}
err = ca.Map("baz")
if err != nil {
t.Error(err)
}
ca.Push()
err = ca.Map("foo")
if err != nil {
t.Error(err)
}
ca.Pop()
err = ca.Map("foo")
if err != nil {
t.Error(err)
}
}

View File

@ -1,8 +1,7 @@
package menu package render
import ( import (
"fmt" "fmt"
"log"
) )
// BrowseConfig defines the availability and display parameters for page browsing. // BrowseConfig defines the availability and display parameters for page browsing.
@ -33,6 +32,7 @@ type Menu struct {
pageCount uint16 pageCount uint16
canNext bool canNext bool
canPrevious bool canPrevious bool
outputSize uint16
} }
// NewMenu creates a new Menu with an explicit page count. // NewMenu creates a new Menu with an explicit page count.
@ -46,17 +46,21 @@ func(m *Menu) WithPageCount(pageCount uint16) *Menu {
return m return m
} }
// WithSize defines the maximum byte size of the rendered menu.
func(m *Menu) WithOutputSize(outputSize uint16) *Menu {
m.outputSize = outputSize
return m
}
// WithBrowseConfig defines the criteria for page browsing. // WithBrowseConfig defines the criteria for page browsing.
func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu { func(m *Menu) WithBrowseConfig(cfg BrowseConfig) *Menu {
m.browse = cfg m.browse = cfg
return m return m
} }
// Put adds a menu option to the menu rendering. // Put adds a menu option to the menu rendering.
func(m *Menu) Put(selector string, title string) error { func(m *Menu) Put(selector string, title string) error {
m.menu = append(m.menu, [2]string{selector, title}) m.menu = append(m.menu, [2]string{selector, title})
log.Printf("menu %v", m.menu)
return nil return nil
} }
@ -64,6 +68,11 @@ func(m *Menu) Put(selector string, title string) error {
// //
// After this has been executed, the state of the menu will be empty. // After this has been executed, the state of the menu will be empty.
func(m *Menu) Render(idx uint16) (string, error) { func(m *Menu) Render(idx uint16) (string, error) {
var menuCopy [][2]string
for _, v := range m.menu {
menuCopy = append(menuCopy, v)
}
err := m.applyPage(idx) err := m.applyPage(idx)
if err != nil { if err != nil {
return "", err return "", err
@ -81,28 +90,30 @@ func(m *Menu) Render(idx uint16) (string, error) {
} }
r += fmt.Sprintf("%s:%s", choice, title) r += fmt.Sprintf("%s:%s", choice, title)
} }
m.menu = menuCopy
return r, nil return r, nil
} }
// add available browse options. // add available browse options.
func(m *Menu) applyPage(idx uint16) error { func(m *Menu) applyPage(idx uint16) error {
if m.pageCount == 0 { if m.pageCount == 0 {
if idx > 0 {
return fmt.Errorf("index %v > 0 for non-paged menu", idx)
}
return nil return nil
} else if idx >= m.pageCount { } else if idx >= m.pageCount {
return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount) return fmt.Errorf("index %v out of bounds (%v)", idx, m.pageCount)
} }
if m.browse.NextAvailable {
m.canNext = true m.reset()
}
if m.browse.PreviousAvailable {
m.canPrevious = true
}
if idx == m.pageCount - 1 { if idx == m.pageCount - 1 {
m.canNext = false m.canNext = false
} }
if idx == 0 { if idx == 0 {
m.canPrevious = false m.canPrevious = false
} }
if m.canNext { if m.canNext {
err := m.Put(m.browse.NextSelector, m.browse.NextTitle) err := m.Put(m.browse.NextSelector, m.browse.NextTitle)
if err != nil { if err != nil {
@ -129,4 +140,15 @@ func(m *Menu) shiftMenu() (string, string, error) {
return r[0], r[1], nil return r[0], r[1], nil
} }
func(m *Menu) reset() {
if m.browse.NextAvailable {
m.canNext = true
}
if m.browse.PreviousAvailable {
m.canPrevious = true
}
}
func(m *Menu) ReservedSize() uint16 {
return m.outputSize
}

View File

@ -1,4 +1,4 @@
package menu package render
import ( import (
"testing" "testing"
@ -23,6 +23,12 @@ func TestMenuInit(t *testing.T) {
if r != expect { if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
} }
r, err = m.Render(1)
if err == nil {
t.Fatalf("expected render fail")
}
} }
func TestMenuBrowse(t *testing.T) { func TestMenuBrowse(t *testing.T) {
@ -48,14 +54,6 @@ func TestMenuBrowse(t *testing.T) {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
} }
err = m.Put("1", "foo")
if err != nil {
t.Fatal(err)
}
err = m.Put("2", "bar")
if err != nil {
t.Fatal(err)
}
r, err = m.Render(1) r, err = m.Render(1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -68,14 +66,6 @@ func TestMenuBrowse(t *testing.T) {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r) t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
} }
err = m.Put("1", "foo")
if err != nil {
t.Fatal(err)
}
err = m.Put("2", "bar")
if err != nil {
t.Fatal(err)
}
r, err = m.Render(2) r, err = m.Render(2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

289
go/render/page.go Normal file
View File

@ -0,0 +1,289 @@
package render
import (
"bytes"
"fmt"
"log"
"strings"
"text/template"
"git.defalsify.org/festive/cache"
"git.defalsify.org/festive/resource"
)
type Page struct {
cacheMap map[string]string // Mapped
cache *cache.Cache
resource resource.Resource
menu *Menu
sink *string
sinkSize uint16
sizer *Sizer
sinkProcessed bool
}
func NewPage(cache *cache.Cache, rs resource.Resource) *Page {
return &Page{
cache: cache,
cacheMap: make(map[string]string),
resource: rs,
}
}
func(pg *Page) WithMenu(menu *Menu) *Page {
pg.menu = menu
if pg.sizer != nil {
pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
}
return pg
}
func(pg *Page) WithSizer(sizer *Sizer) *Page {
pg.sizer = sizer
if pg.menu != nil {
pg.sizer = pg.sizer.WithMenuSize(pg.menu.ReservedSize())
}
return pg
}
// Size returns size used by values and menu, and remaining size available
func(pg *Page) Usage() (uint32, uint32, error) {
var l int
var c uint16
for k, v := range pg.cacheMap {
l += len(v)
sz, err := pg.cache.ReservedSize(k)
if err != nil {
return 0, 0, err
}
c += sz
}
r := uint32(l)
if pg.menu != nil {
r += uint32(pg.menu.ReservedSize())
}
return r, uint32(c)-r, nil
}
// Map marks the given key for retrieval.
//
// After this, Val() will return the value for the key, and Size() will include the value size and limitations in its calculations.
//
// Only one symbol with no size limitation may be mapped at the current level.
func(pg *Page) Map(key string) error {
m, err := pg.cache.Get()
if err != nil {
return err
}
l, err := pg.cache.ReservedSize(key)
if err != nil {
return err
}
if l == 0 {
if pg.sink != nil {
return fmt.Errorf("sink already set to symbol '%v'", *pg.sink)
}
pg.sink = &key
}
pg.cacheMap[key] = m[key]
if pg.sizer != nil {
err := pg.sizer.Set(key, l)
if err != nil {
return err
}
}
return nil
}
// Fails if key is not mapped.
func(pg *Page) Val(key string) (string, error) {
r := pg.cacheMap[key]
if len(r) == 0 {
return "", fmt.Errorf("key %v not mapped", key)
}
return r, nil
}
// Moved from cache, MAP should hook to this object
func(pg *Page) Sizes() (map[string]uint16, error) {
sizes := make(map[string]uint16)
var haveSink bool
for k, _ := range pg.cacheMap {
l, err := pg.cache.ReservedSize(k)
if err != nil {
return nil, err
}
if l == 0 {
if haveSink {
panic(fmt.Sprintf("duplicate sink for %v", k))
}
haveSink = true
}
pg.sinkSize = l
}
return sizes, nil
}
// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate.
func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16) (string, error) {
tpl, err := pg.resource.GetTemplate(sym)
if err != nil {
return "", err
}
if pg.sizer != nil {
values, err = pg.sizer.GetAt(values, idx)
if err != nil {
return "", err
}
} else if idx > 0 {
return "", fmt.Errorf("sizer needed for indexed render")
}
log.Printf("render for index: %v", idx)
tp, err := template.New("tester").Option("missingkey=error").Parse(tpl)
if err != nil {
return "", err
}
b := bytes.NewBuffer([]byte{})
err = tp.Execute(b, values)
if err != nil {
return "", err
}
return b.String(), err
}
// render menu and all syms except sink, split sink into display chunks
func(pg *Page) prepare(sym string, values map[string]string, idx uint16) (map[string]string, error) {
var sink string
var sinkValues []string
noSinkValues := make(map[string]string)
for k, v := range values {
sz, err := pg.cache.ReservedSize(k)
if err != nil {
return nil, err
}
if sz == 0 {
sink = k
sinkValues = strings.Split(v, "\n")
v = ""
log.Printf("found sink %s with field count %v", k, len(sinkValues))
}
noSinkValues[k] = v
}
if sink == "" {
log.Printf("no sink found for sym %s", sym)
return values, nil
}
pg.sizer.AddCursor(0)
s, err := pg.render(sym, noSinkValues, 0)
if err != nil {
return nil, err
}
remaining, ok := pg.sizer.Check(s)
if !ok {
return nil, fmt.Errorf("capacity exceeded")
}
log.Printf("%v bytes available for sink split", remaining)
l := 0
var count uint16
tb := strings.Builder{}
rb := strings.Builder{}
for i, v := range sinkValues {
log.Printf("processing sinkvalue %v: %s", i, v)
l += len(v)
if uint32(l) > remaining {
if tb.Len() == 0 {
return nil, fmt.Errorf("capacity insufficient for sink field %v", i)
}
rb.WriteString(tb.String())
rb.WriteRune('\n')
c := uint32(rb.Len())
pg.sizer.AddCursor(c)
tb.Reset()
l = 0
count += 1
}
if tb.Len() > 0 {
tb.WriteByte(byte(0x00))
}
tb.WriteString(v)
}
if tb.Len() > 0 {
rb.WriteString(tb.String())
count += 1
}
r := rb.String()
r = strings.TrimRight(r, "\n")
noSinkValues[sink] = r
if pg.menu != nil {
pg.menu = pg.menu.WithPageCount(count)
}
for i, v := range strings.Split(r, "\n") {
log.Printf("nosinkvalue %v: %s", i, v)
}
return noSinkValues, nil
}
func(pg *Page) render(sym string, values map[string]string, idx uint16) (string, error) {
var ok bool
r := ""
s, err := pg.RenderTemplate(sym, values, idx)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for template", len(s))
r += s
if pg.sizer != nil {
_, ok = pg.sizer.Check(r)
if !ok {
return "", fmt.Errorf("limit exceeded: %v", pg.sizer)
}
}
s, err = pg.menu.Render(idx)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for menu", len(s))
r += "\n" + s
if pg.sizer != nil {
_, ok = pg.sizer.Check(r)
if !ok {
return "", fmt.Errorf("limit exceeded: %v", pg.sizer)
}
}
return r, nil
}
func(pg *Page) Render(sym string, values map[string]string, idx uint16) (string, error) {
var err error
values, err = pg.prepare(sym, values, idx)
if err != nil {
return "", err
}
log.Printf("nosink %v", values)
return pg.render(sym, values, idx)
}
func(pg *Page) Reset() {
pg.sink = nil
pg.sinkSize = 0
pg.sinkProcessed = false
pg.cacheMap = make(map[string]string)
}

View File

@ -1,12 +1,10 @@
package resource package render
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log" "log"
"strings" "strings"
"git.defalsify.org/festive/state"
) )
type Sizer struct { type Sizer struct {
@ -18,28 +16,34 @@ type Sizer struct {
sink string sink string
} }
func SizerFromState(st *state.State) (Sizer, error){ func NewSizer(outputSize uint32) *Sizer {
sz := Sizer{ return &Sizer{
outputSize: st.GetOutputSize(), outputSize: outputSize,
menuSize: st.GetMenuSize(),
memberSizes: make(map[string]uint16), memberSizes: make(map[string]uint16),
} }
}
sizes, err := st.Sizes() func(szr *Sizer) WithMenuSize(menuSize uint16) *Sizer {
if err != nil { szr.menuSize = menuSize
return sz, err return szr
}
func(szr *Sizer) Set(key string, size uint16) error {
var ok bool
_, ok = szr.memberSizes[key]
if ok {
return fmt.Errorf("already have key %s", key)
} }
for k, v := range sizes { szr.memberSizes[key] = size
if v == 0 { if size == 0 {
sz.sink = k szr.sink = key
}
sz.memberSizes[k] = v
sz.totalMemberSize += uint32(v)
} }
return sz, nil szr.totalMemberSize += uint32(size)
return nil
} }
func(szr *Sizer) Check(s string) (uint32, bool) { func(szr *Sizer) Check(s string) (uint32, bool) {
log.Printf("sizercheck %s", s)
l := uint32(len(s)) l := uint32(len(s))
if szr.outputSize > 0 { if szr.outputSize > 0 {
if l > szr.outputSize { if l > szr.outputSize {

181
go/render/size_test.go Normal file
View File

@ -0,0 +1,181 @@
package render
import (
"context"
"fmt"
"testing"
"git.defalsify.org/festive/state"
"git.defalsify.org/festive/resource"
"git.defalsify.org/festive/cache"
)
type TestSizeResource struct {
*resource.MenuResource
}
func getTemplate(sym string) (string, error) {
var tpl string
switch sym {
case "small":
tpl = "one {{.foo}} two {{.bar}} three {{.baz}}"
case "toobig":
tpl = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus in mattis lorem. Aliquam erat volutpat. Ut vitae metus."
case "pages":
tpl = "one {{.foo}} two {{.bar}} three {{.baz}}\n{{.xyzzy}}"
}
return tpl, nil
}
func funcFor(sym string) (resource.EntryFunc, error) {
switch sym {
case "foo":
return getFoo, nil
case "bar":
return getBar, nil
case "baz":
return getBaz, nil
case "xyzzy":
return getXyzzy, nil
}
return nil, fmt.Errorf("unknown func: %s", sym)
}
func getFoo(ctx context.Context) (string, error) {
return "inky", nil
}
func getBar(ctx context.Context) (string, error) {
return "pinky", nil
}
func getBaz(ctx context.Context) (string, error) {
return "blinky", nil
}
func getXyzzy(ctx context.Context) (string, error) {
return "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", nil
}
func TestSizeCheck(t *testing.T) {
szr := NewSizer(16)
l, ok := szr.Check("foobar")
if !ok {
t.Fatalf("expected ok")
}
if l != 10 {
t.Fatalf("expected 10, got %v", l)
}
l, ok = szr.Check("inkypinkyblinkyclyde")
if ok {
t.Fatalf("expected not ok")
}
if l != 0 {
t.Fatalf("expected 0, got %v", l)
}
}
func TestSizeLimit(t *testing.T) {
st := state.NewState(0)
ca := cache.NewCache()
mn := NewMenu().WithOutputSize(32)
mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
szr := NewSizer(128)
pg := NewPage(ca, rs).WithMenu(mn).WithSizer(szr)
ca.Push()
st.Down("test")
ca.Add("foo", "inky", 4)
ca.Add("bar", "pinky", 10)
ca.Add("baz", "blinky", 0)
pg.Map("foo")
pg.Map("bar")
pg.Map("baz")
mn.Put("1", "foo the foo")
mn.Put("2", "go to bar")
vals, err := ca.Get()
if err != nil {
t.Fatal(err)
}
_, err = pg.Render("small", vals, 0)
if err != nil {
t.Fatal(err)
}
mn.Put("1", "foo the foo")
mn.Put("2", "go to bar")
_, err = pg.Render("toobig", vals, 0)
if err == nil {
t.Fatalf("expected size exceeded")
}
}
func TestSizePages(t *testing.T) {
st := state.NewState(0)
ca := cache.NewCache()
mn := NewMenu().WithOutputSize(32)
mrs := resource.NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
szr := NewSizer(128)
pg := NewPage(ca, rs).WithSizer(szr).WithMenu(mn)
ca.Push()
st.Down("test")
ca.Add("foo", "inky", 4)
ca.Add("bar", "pinky", 10)
ca.Add("baz", "blinky", 20)
ca.Add("xyzzy", "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", 0)
pg.Map("foo")
pg.Map("bar")
pg.Map("baz")
pg.Map("xyzzy")
vals, err := ca.Get()
if err != nil {
t.Fatal(err)
}
mn.Put("1", "foo the foo")
mn.Put("2", "go to bar")
r, err := pg.Render("pages", vals, 0)
if err != nil {
t.Fatal(err)
}
expect := `one inky two pinky three blinky
inky pinky
blinky clyde sue
tinkywinky dipsy
lala poo
1:foo the foo
2:go to bar`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
r, err = pg.Render("pages", vals, 1)
if err != nil {
t.Fatal(err)
}
expect = `one inky two pinky three blinky
one two three four five six seven
eight nine ten
eleven twelve
1:foo the foo
2:go to bar`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
}

View File

@ -23,7 +23,7 @@ func NewFsResource(path string) (FsResource) {
} }
} }
func(fs FsResource) GetTemplate(sym string, sizer *Sizer) (string, error) { func(fs FsResource) GetTemplate(sym string) (string, error) {
fp := path.Join(fs.Path, sym) fp := path.Join(fs.Path, sym)
r, err := ioutil.ReadFile(fp) r, err := ioutil.ReadFile(fp)
s := string(r) s := string(r)

View File

@ -1,40 +1,40 @@
package resource package resource
import ( //import (
"bytes" // "bytes"
"fmt" // "fmt"
"log" // "log"
"text/template" // "text/template"
) //)
// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. //// DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate.
func DefaultRenderTemplate(r Resource, sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) { //func DefaultRenderTemplate(r Resource, sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) {
v, err := r.GetTemplate(sym, nil) // v, err := r.GetTemplate(sym, nil)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
//
if sizer != nil { // if sizer != nil {
values, err = sizer.GetAt(values, idx) // values, err = sizer.GetAt(values, idx)
} else if idx > 0 { // } else if idx > 0 {
return "", fmt.Errorf("sizer needed for indexed render") // return "", fmt.Errorf("sizer needed for indexed render")
} // }
log.Printf("render for index: %v", idx) // log.Printf("render for index: %v", idx)
//
if err != nil { // if err != nil {
return "", err // return "", err
} // }
//
tp, err := template.New("tester").Option("missingkey=error").Parse(v) // tp, err := template.New("tester").Option("missingkey=error").Parse(v)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
//
b := bytes.NewBuffer([]byte{}) // b := bytes.NewBuffer([]byte{})
err = tp.Execute(b, values) // err = tp.Execute(b, values)
if err != nil { // if err != nil {
return "", err // return "", err
} // }
return b.String(), err // return b.String(), err
} //}

View File

@ -2,23 +2,18 @@ package resource
import ( import (
"context" "context"
"fmt"
"log"
"strings"
) )
// EntryFunc is a function signature for retrieving value for a key // EntryFunc is a function signature for retrieving value for a key
type EntryFunc func(ctx context.Context) (string, error) type EntryFunc func(ctx context.Context) (string, error)
type CodeFunc func(sym string) ([]byte, error) type CodeFunc func(sym string) ([]byte, error)
type TemplateFunc func(sym string, sizer *Sizer) (string, error) type TemplateFunc func(sym string) (string, error)
type FuncForFunc func(sym string) (EntryFunc, error) type FuncForFunc func(sym string) (EntryFunc, error)
// Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. // Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries.
type Resource interface { type Resource interface {
GetTemplate(sym string, sizer *Sizer) (string, error) // Get the template for a given symbol. GetTemplate(sym string) (string, error) // Get the template for a given symbol.
GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol. GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol.
RenderTemplate(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) // Render the given data map using the template of the symbol.
Render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) // Render full output.
FuncFor(sym string) (EntryFunc, error) // Resolve symbol content point for. FuncFor(sym string) (EntryFunc, error) // Resolve symbol content point for.
} }
@ -52,87 +47,6 @@ func(m *MenuResource) WithTemplateGetter(templateGetter TemplateFunc) *MenuResou
return m return m
} }
// render menu and all syms except sink, split sink into display chunks
func(m *MenuResource) prepare(sym string, values map[string]string, idx uint16, sizer *Sizer) (map[string]string, error) {
var sink string
var sinkValues []string
noSinkValues := make(map[string]string)
for k, v := range values {
sz, err := sizer.Size(k)
if err != nil {
return nil, err
}
if sz == 0 {
sink = k
sinkValues = strings.Split(v, "\n")
v = ""
log.Printf("found sink %s with field count %v", k, len(sinkValues))
}
noSinkValues[k] = v
}
if sink == "" {
log.Printf("no sink found for sym %s", sym)
return values, nil
}
s, err := m.render(sym, noSinkValues, 0, nil)
if err != nil {
return nil, err
}
remaining, ok := sizer.Check(s)
if !ok {
return nil, fmt.Errorf("capacity exceeded")
}
log.Printf("%v bytes available for sink split", remaining)
l := 0
tb := strings.Builder{}
rb := strings.Builder{}
sizer.AddCursor(0)
for i, v := range sinkValues {
log.Printf("processing sinkvalue %v: %s", i, v)
l += len(v)
if uint32(l) > remaining {
if tb.Len() == 0 {
return nil, fmt.Errorf("capacity insufficient for sink field %v", i)
}
rb.WriteString(tb.String())
rb.WriteRune('\n')
c := uint32(rb.Len())
sizer.AddCursor(c)
tb.Reset()
l = 0
}
if tb.Len() > 0 {
tb.WriteByte(byte(0x00))
}
tb.WriteString(v)
}
if tb.Len() > 0 {
rb.WriteString(tb.String())
}
r := rb.String()
r = strings.TrimRight(r, "\n")
noSinkValues[sink] = r
for i, v := range strings.Split(r, "\n") {
log.Printf("nosinkvalue %v: %s", i, v)
}
return noSinkValues, nil
}
func(m *MenuResource) RenderTemplate(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) {
return DefaultRenderTemplate(m, sym, values, idx, sizer)
}
func(m *MenuResource) FuncFor(sym string) (EntryFunc, error) { func(m *MenuResource) FuncFor(sym string) (EntryFunc, error) {
return m.funcFunc(sym) return m.funcFunc(sym)
} }
@ -141,47 +55,7 @@ func(m *MenuResource) GetCode(sym string) ([]byte, error) {
return m.codeFunc(sym) return m.codeFunc(sym)
} }
func(m *MenuResource) GetTemplate(sym string, sizer *Sizer) (string, error) { func(m *MenuResource) GetTemplate(sym string) (string, error) {
return m.templateFunc(sym, sizer) return m.templateFunc(sym)
} }
func(m *MenuResource) render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) {
var ok bool
r := ""
s, err := m.RenderTemplate(sym, values, idx, sizer)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for template", len(s))
r += s
if sizer != nil {
_, ok = sizer.Check(r)
if !ok {
return "", fmt.Errorf("limit exceeded: %v", sizer)
}
}
s, err = m.RenderMenu(idx)
if err != nil {
return "", err
}
log.Printf("rendered %v bytes for menu", len(s))
r += s
if sizer != nil {
_, ok = sizer.Check(r)
if !ok {
return "", fmt.Errorf("limit exceeded: %v", sizer)
}
}
return r, nil
}
func(m *MenuResource) Render(sym string, values map[string]string, idx uint16, sizer *Sizer) (string, error) {
var err error
values, err = m.prepare(sym, values, idx, sizer)
if err != nil {
return "", err
}
return m.render(sym, values, idx, sizer)
}

View File

@ -10,7 +10,7 @@ type TestSizeResource struct {
*MenuResource *MenuResource
} }
func getTemplate(sym string, sizer *Sizer) (string, error) { func getTemplate(sym string) (string, error) {
var tpl string var tpl string
switch sym { switch sym {
case "small": case "small":

View File

@ -1,121 +0,0 @@
package resource
import (
"testing"
"git.defalsify.org/festive/state"
)
func TestSizeLimit(t *testing.T) {
st := state.NewState(0).WithOutputSize(128)
mrs := NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
st.Down("test")
st.Add("foo", "inky", 4)
st.Add("bar", "pinky", 10)
st.Add("baz", "blinky", 0)
st.Map("foo")
st.Map("bar")
st.Map("baz")
st.SetMenuSize(32)
szr, err := SizerFromState(&st)
if err != nil {
t.Fatal(err)
}
rs.PutMenu("1", "foo the foo")
rs.PutMenu("2", "go to bar")
tpl, err := rs.GetTemplate("small", &szr)
if err != nil {
t.Fatal(err)
}
vals, err := st.Get()
if err != nil {
t.Fatal(err)
}
_ = tpl
_, err = rs.Render("small", vals, 0, &szr)
if err != nil {
t.Fatal(err)
}
rs.PutMenu("1", "foo the foo")
rs.PutMenu("2", "go to bar")
_, err = rs.Render("toobig", vals, 0, &szr)
if err == nil {
t.Fatalf("expected size exceeded")
}
}
func TestSizePages(t *testing.T) {
st := state.NewState(0).WithOutputSize(128)
mrs := NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate)
rs := TestSizeResource{
mrs,
}
st.Down("test")
st.Add("foo", "inky", 4)
st.Add("bar", "pinky", 10)
st.Add("baz", "blinky", 20)
st.Add("xyzzy", "inky pinky\nblinky clyde sue\ntinkywinky dipsy\nlala poo\none two three four five six seven\neight nine ten\neleven twelve", 0)
st.Map("foo")
st.Map("bar")
st.Map("baz")
st.Map("xyzzy")
st.SetMenuSize(32)
szr, err := SizerFromState(&st)
if err != nil {
t.Fatal(err)
}
vals, err := st.Get()
if err != nil {
t.Fatal(err)
}
rs.PutMenu("1", "foo the foo")
rs.PutMenu("2", "go to bar")
r, err := rs.Render("pages", vals, 0, &szr)
if err != nil {
t.Fatal(err)
}
expect := `one inky two pinky three blinky
inky pinky
blinky clyde sue
tinkywinky dipsy
lala poo`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
rs.PutMenu("1", "foo the foo")
rs.PutMenu("2", "go to bar")
szr, err = SizerFromState(&st)
if err != nil {
t.Fatal(err)
}
r, err = rs.Render("pages", vals, 1, &szr)
if err != nil {
t.Fatal(err)
}
expect = `one inky two pinky three blinky
one two three four five six seven
eight nine ten
eleven twelve`
if r != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, r)
}
}

View File

@ -27,18 +27,3 @@ func(sr *StateResource) WithState(st *state.State) *StateResource {
sr.st = st sr.st = st
return sr return sr
} }
func(sr *StateResource) SetMenuBrowse(selector string, title string, back bool) error {
var err error
next, prev := sr.st.Sides()
if back {
if prev {
err = sr.Resource.SetMenuBrowse(selector, title, true)
}
} else if next {
err = sr.Resource.SetMenuBrowse(selector, title, false)
}
return err
}

View File

@ -12,90 +12,3 @@ func TestStateResourceInit(t *testing.T) {
_ = ToStateResource(rs).WithState(&st) _ = ToStateResource(rs).WithState(&st)
_ = NewStateResource(&st) _ = NewStateResource(&st)
} }
func TestStateBrowseNoSink(t *testing.T) {
st := state.NewState(0)
st.Down("root")
rs := NewStateResource(&st)
rs.PutMenu("1", "foo")
rs.PutMenu("2", "bar")
err := rs.SetMenuBrowse("11", "next", false)
if err != nil {
t.Fatal(err)
}
err = rs.SetMenuBrowse("22", "prev", true)
if err != nil {
t.Fatal(err)
}
s, err := rs.RenderMenu(0)
if err != nil {
t.Fatal(err)
}
expect := `1:foo
2:bar`
if s != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s)
}
}
func TestStateBrowseSink(t *testing.T) {
st := state.NewState(0)
st.Down("root")
rs := NewStateResource(&st)
rs.PutMenu("1", "foo")
rs.PutMenu("2", "bar")
err := rs.SetMenuBrowse("11", "next", false)
if err != nil {
t.Fatal(err)
}
err = rs.SetMenuBrowse("22", "prev", true)
if err != nil {
t.Fatal(err)
}
s, err := rs.RenderMenu(0)
if err != nil {
t.Fatal(err)
}
expect := `1:foo
2:bar
11:next`
if s != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s)
}
idx, err := st.Next()
if err != nil {
t.Fatal(err)
}
if idx != 1 {
t.Fatalf("expected idx 1, got %v", idx)
}
rs = NewStateResource(&st)
rs.PutMenu("1", "foo")
rs.PutMenu("2", "bar")
err = rs.SetMenuBrowse("11", "next", false)
if err != nil {
t.Fatal(err)
}
err = rs.SetMenuBrowse("22", "prev", true)
if err != nil {
t.Fatal(err)
}
s, err = rs.RenderMenu(idx)
if err != nil {
t.Fatal(err)
}
expect = `1:foo
2:bar
11:next
22:prev`
if s != expect {
t.Fatalf("expected:\n\t%s\ngot:\n\t%s\n", expect, s)
}
}