Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

batch.gno

4.42 Kb · 144 lines
  1package governor
  2
  3import (
  4	"strings"
  5
  6	ufmt "gno.land/p/nt/ufmt/v0"
  7)
  8
  9// batchKind runs several adopted kinds as one decision.
 10//
 11//	govern:batch   "treasury/spend 100\nfee 250"
 12//
 13// One member per line, the kind first and the rest its payload. Members must
 14// already be adopted: a batch decides several things at once and smuggles in
 15// nothing.
 16//
 17// All or nothing, because there is no halfway. A member's Do returning an error
 18// makes this panic, aborting the transaction and unwinding what earlier members
 19// did — the only atomicity gno offers.
 20//
 21// So a failed batch leaves the proposal Succeeded rather than Failed: the
 22// transaction wrote nothing, including the failure. It can be run again and
 23// will fail identically until the world changes or the grace period ends it.
 24//
 25// Single-member kinds keep the other behaviour — record the failure and finish
 26// — since there is nothing to be atomic with respect to.
 27type batchKind struct{}
 28
 29// maxBatch bounds a batch, because Describe and Check both walk it and a
 30// proposal nobody can render is a proposal nobody can vote on.
 31const maxBatch = 16
 32
 33func (k batchKind) Name() string { return reserved + "batch" }
 34
 35func (k batchKind) describe(g *Governor, payload string) string {
 36	members, err := parseBatch(payload)
 37	if err != nil {
 38		return "malformed batch: " + err.Error()
 39	}
 40	out := ufmt.Sprintf("do all %d of these, or none of them:\n", len(members))
 41	for i, m := range members {
 42		// Each member described by its own kind, from its own payload — the
 43		// same strings their Do will be handed. A batch that summarised its
 44		// members in its own words would be the description gap this design
 45		// exists to avoid, reintroduced one level up.
 46		out += ufmt.Sprintf("\n%d. **%s** — %s", i+1, m.kind, g.describeMember(m))
 47	}
 48	return out
 49}
 50
 51func (g *Governor) describeMember(m member) string {
 52	k := g.anyKind(m.kind)
 53	if k == nil {
 54		return "_no kind by that name_"
 55	}
 56	return g.describeKind(k, m.payload)
 57}
 58
 59func (k batchKind) check(g *Governor, payload string) error {
 60	members, err := parseBatch(payload)
 61	if err != nil {
 62		return err
 63	}
 64	for _, m := range members {
 65		e := g.entryOf(m.kind)
 66		if e == nil {
 67			return govErr("no kind called " + m.kind)
 68		}
 69		if !e.live {
 70			// A batch cannot reach a power the holders have not granted.
 71			return govErr(m.kind + " is not adopted")
 72		}
 73		if m.kind == (batchKind{}).Name() {
 74			// No batches of batches. Nesting makes the rendered description a
 75			// tree of unbounded depth, and the point of the description is
 76			// that somebody reads it.
 77			return govErr("a batch cannot contain a batch")
 78		}
 79		if err := g.checkKind(e.kind, m.payload); err != nil {
 80			return govErr(m.kind + ": " + err.Error())
 81		}
 82	}
 83	return nil
 84}
 85
 86func (k batchKind) run(g *Governor, dispatch Dispatch, payload string) error {
 87	members, err := parseBatch(payload)
 88	if err != nil {
 89		return err
 90	}
 91	for i, m := range members {
 92		e := g.entryOf(m.kind)
 93		if e == nil || !e.live {
 94			panic("govern: batch member " + m.kind + " is no longer adopted")
 95		}
 96		var err error
 97		if b, ok := asBuiltin(e.kind); ok {
 98			err = b.run(g, dispatch, m.payload)
 99		} else {
100			// Each member gets its OWN sub-realm, named for the power it is,
101			// rather than the batch's. A batch is a way to decide several
102			// things at once, not a way to launder one kind's authority into
103			// another's.
104			err = dispatch(e.kind, subPathOf(m.kind), m.payload)
105		}
106		if err != nil {
107			// Panic, not return. Returning would record a failure and keep
108			// whatever the earlier members already did, which is the one
109			// outcome a batch promises cannot happen.
110			panic(ufmt.Sprintf("govern: batch member %d (%s) failed: %s",
111				i+1, m.kind, err.Error()))
112		}
113	}
114	return nil
115}
116
117type member struct {
118	kind    string
119	payload string
120}
121
122func parseBatch(payload string) ([]member, error) {
123	var out []member
124	for _, line := range strings.Split(payload, "\n") {
125		line = strings.TrimSpace(line)
126		if line == "" {
127			continue
128		}
129		sp := strings.Index(line, " ")
130		if sp <= 0 {
131			// A member with no payload is almost always a typo, and a batch is
132			// the wrong place to find out.
133			return nil, govErr("expected `<kind> <payload>`, got: " + line)
134		}
135		out = append(out, member{kind: line[:sp], payload: strings.TrimSpace(line[sp+1:])})
136	}
137	if len(out) < 2 {
138		return nil, govErr("a batch needs at least two members; propose the kind directly")
139	}
140	if len(out) > maxBatch {
141		return nil, govErr("too many members in one batch")
142	}
143	return out, nil
144}