1package app23import (4 "errors"5 "strconv"6 "strings"7)89// A schema is what a handler says about itself: the verbs it answers, what10// each one takes, and what it gives back. Handlers declare one; the app reads11// it to enumerate the API, to validate payloads before dispatch, and to refuse12// an upgrade that would break a caller.13//14// This is the part a verb-dispatch API is usually missing. Without it, "the15// API" is whatever the live handler's switch statement happens to accept: not16// discoverable, not checkable, and free to change under callers without17// anything noticing. A schema makes it data, which is the same move that makes18// the API growable in the first place.19//20// What it is not is static typing. Gno has no reflection, so nothing here can21// look at your Go types. Validation happens at call time against declarations22// you wrote by hand, and a handler whose switch disagrees with its own schema23// is a bug the schema cannot catch.2425// Kind is the type of one payload field. The set is deliberately small: these26// are the types that survive a string encoding without ambiguity.27type Kind string2829const (30 KindString Kind = "string"31 KindInt Kind = "int" // int64, base 1032 KindBool Kind = "bool" // "true" or "false"33 KindAddress Kind = "address" // bech32, validated34 KindNone Kind = "none" // a verb that returns nothing meaningful35)3637// Valid reports whether k is a known kind.38func (k Kind) Valid() bool {39 switch k {40 case KindString, KindInt, KindBool, KindAddress, KindNone:41 return true42 }43 return false44}4546// parse checks that v is a well-formed value of kind k.47func (k Kind) parse(v string) error {48 switch k {49 case KindString, KindNone:50 return nil51 case KindInt:52 if _, err := strconv.ParseInt(v, 10, 64); err != nil {53 return errors.New("expected an integer, got " + strconv.Quote(v))54 }55 case KindBool:56 if v != "true" && v != "false" {57 return errors.New("expected true or false, got " + strconv.Quote(v))58 }59 case KindAddress:60 if !address(v).IsValid() {61 return errors.New("expected an address, got " + strconv.Quote(v))62 }63 default:64 return errors.New("unknown kind " + string(k))65 }66 return nil67}6869// Field is one parameter of a verb.70type Field struct {71 Name string72 Kind Kind73 Doc string74 Opt bool // may be omitted, and only at the end of the parameter list75}7677// P declares a required parameter.78func P(name string, kind Kind, doc string) Field {79 return Field{Name: name, Kind: kind, Doc: doc}80}8182// Opt declares an optional parameter. Optional parameters must come last, and83// appending one is the only way to add a parameter to a verb without breaking84// callers.85func Opt(name string, kind Kind, doc string) Field {86 return Field{Name: name, Kind: kind, Doc: doc, Opt: true}87}8889// Verb is one operation's signature.90type Verb struct {91 Name string92 Doc string93 Result Kind94 Params []Field95}9697// Op declares a verb.98func Op(name, doc string, result Kind, params ...Field) Verb {99 return Verb{Name: name, Doc: doc, Result: result, Params: params}100}101102// Required returns how many leading parameters must be supplied.103func (v Verb) Required() int {104 n := 0105 for _, p := range v.Params {106 if p.Opt {107 break108 }109 n++110 }111 return n112}113114// Signature renders the verb as a one-line declaration:115//116// add(text:string, urgent:bool?) -> int117func (v Verb) Signature() string {118 var b strings.Builder119 b.WriteString(v.Name)120 b.WriteString("(")121 for i, p := range v.Params {122 if i > 0 {123 b.WriteString(", ")124 }125 b.WriteString(p.Name + ":" + string(p.Kind))126 if p.Opt {127 b.WriteString("?")128 }129 }130 b.WriteString(") -> " + string(v.Result))131 return b.String()132}133134// Schema is a handler's whole API, sorted by verb name.135type Schema struct {136 verbs []Verb137}138139// NewSchema validates and sorts the declarations. It panics on a schema that140// could not be served correctly: a blank or duplicate name, an unknown kind, a141// required parameter after an optional one, or a duplicate parameter name.142// Those are all authoring mistakes, and a handler carrying one should fail to143// deploy rather than mis-describe itself forever.144func NewSchema(verbs ...Verb) *Schema {145 s := &Schema{verbs: make([]Verb, 0, len(verbs))}146 for _, v := range verbs {147 if strings.TrimSpace(v.Name) == "" || v.Name != strings.TrimSpace(v.Name) {148 panic(errors.New("app: verb names must be non-blank and unpadded"))149 }150 if !v.Result.Valid() {151 panic(errors.New("app: verb " + v.Name + ": unknown result kind " + string(v.Result)))152 }153 if _, dup := s.Lookup(v.Name); dup {154 panic(errors.New("app: duplicate verb " + v.Name))155 }156 seenOpt := false157 for i, p := range v.Params {158 if strings.TrimSpace(p.Name) == "" || p.Name != strings.TrimSpace(p.Name) {159 panic(errors.New("app: verb " + v.Name + ": parameter names must be non-blank and unpadded"))160 }161 if !p.Kind.Valid() || p.Kind == KindNone {162 panic(errors.New("app: verb " + v.Name + ": parameter " + p.Name +163 " has unusable kind " + string(p.Kind)))164 }165 if p.Opt {166 seenOpt = true167 } else if seenOpt {168 // The encoding is positional, so a required parameter after an169 // optional one could never be supplied without the optional.170 panic(errors.New("app: verb " + v.Name + ": required parameter " +171 p.Name + " follows an optional one"))172 }173 for j := 0; j < i; j++ {174 if v.Params[j].Name == p.Name {175 panic(errors.New("app: verb " + v.Name + ": duplicate parameter " + p.Name))176 }177 }178 }179 at := len(s.verbs)180 for i, e := range s.verbs {181 if v.Name < e.Name {182 at = i183 break184 }185 }186 s.verbs = append(s.verbs, Verb{})187 copy(s.verbs[at+1:], s.verbs[at:])188 s.verbs[at] = v189 }190 return s191}192193// Names returns the verb names, sorted.194func (s *Schema) Names() []string {195 if s == nil {196 return nil197 }198 out := make([]string, 0, len(s.verbs))199 for _, v := range s.verbs {200 out = append(out, v.Name)201 }202 return out203}204205// Lookup returns the verb by name.206func (s *Schema) Lookup(name string) (Verb, bool) {207 if s == nil {208 return Verb{}, false209 }210 for _, v := range s.verbs {211 if v.Name == name {212 return v, true213 }214 }215 return Verb{}, false216}217218// Verbs returns a copy of the declarations.219func (s *Schema) Verbs() []Verb {220 if s == nil {221 return nil222 }223 dup := make([]Verb, len(s.verbs))224 copy(dup, s.verbs)225 return dup226}227228// Signature renders one verb, or "" if it is not declared.229func (s *Schema) Signature(name string) string {230 if v, ok := s.Lookup(name); ok {231 return v.Signature()232 }233 return ""234}235236// Markdown renders the API as a table, for Render.237func (s *Schema) Markdown() string {238 if s == nil || len(s.verbs) == 0 {239 return "_No API declared._\n"240 }241 var b strings.Builder242 b.WriteString("| Verb | Payload | Returns | |\n|---|---|---|---|\n")243 for _, v := range s.verbs {244 params := make([]string, 0, len(v.Params))245 for _, p := range v.Params {246 f := p.Name + ":" + string(p.Kind)247 if p.Opt {248 f += "?"249 }250 params = append(params, f)251 }252 p := strings.Join(params, ", ")253 if p == "" {254 p = "—"255 }256 b.WriteString("| `" + v.Name + "` | `" + p + "` | `" + string(v.Result) +257 "` | " + v.Doc + " |\n")258 }259 return b.String()260}261262// JSON renders the schema for tooling: a client can build a form from this263// without reading any Gno source.264func (s *Schema) JSON() string {265 var b strings.Builder266 b.WriteString(`{"verbs":[`)267 if s != nil {268 for i, v := range s.verbs {269 if i > 0 {270 b.WriteString(",")271 }272 b.WriteString(`{"name":` + jsonStr(v.Name) +273 `,"doc":` + jsonStr(v.Doc) +274 `,"result":` + jsonStr(string(v.Result)) +275 `,"params":[`)276 for j, p := range v.Params {277 if j > 0 {278 b.WriteString(",")279 }280 b.WriteString(`{"name":` + jsonStr(p.Name) +281 `,"kind":` + jsonStr(string(p.Kind)) +282 `,"doc":` + jsonStr(p.Doc) +283 `,"optional":` + strconv.FormatBool(p.Opt) + `}`)284 }285 b.WriteString(`]}`)286 }287 }288 b.WriteString(`]}`)289 return b.String()290}291292// Diff reports what upgrading from prev to s would break for existing callers.293// An empty result means every payload that worked before still works.294//295// Breaking: a verb disappears, a parameter disappears or changes kind or296// position, a new required parameter appears, or a result kind changes.297// Not breaking: a new verb, a new optional parameter at the end, new298// documentation.299func (s *Schema) Diff(prev *Schema) []string {300 out := []string{}301 if prev == nil {302 return out303 }304 for _, old := range prev.verbs {305 nv, ok := s.Lookup(old.Name)306 if !ok {307 out = append(out, "verb "+old.Name+" removed")308 continue309 }310 if nv.Result != old.Result {311 out = append(out, "verb "+old.Name+": result changed from "+312 string(old.Result)+" to "+string(nv.Result))313 }314 for i, op := range old.Params {315 if i >= len(nv.Params) {316 out = append(out, "verb "+old.Name+": parameter "+op.Name+" removed")317 continue318 }319 np := nv.Params[i]320 if np.Name != op.Name {321 out = append(out, "verb "+old.Name+": parameter "+strconv.Itoa(i)+322 " renamed from "+op.Name+" to "+np.Name)323 }324 if np.Kind != op.Kind {325 out = append(out, "verb "+old.Name+": parameter "+op.Name+326 " changed from "+string(op.Kind)+" to "+string(np.Kind))327 }328 if np.Opt != op.Opt && !np.Opt {329 out = append(out, "verb "+old.Name+": parameter "+op.Name+330 " became required")331 }332 }333 for i := len(old.Params); i < len(nv.Params); i++ {334 if !nv.Params[i].Opt {335 out = append(out, "verb "+old.Name+": required parameter "+336 nv.Params[i].Name+" added")337 }338 }339 }340 return out341}342343// jsonStr quotes and escapes a string for the JSON rendering above.344func jsonStr(s string) string {345 var b strings.Builder346 b.WriteString(`"`)347 for _, r := range s {348 switch r {349 case '"':350 b.WriteString(`\"`)351 case '\\':352 b.WriteString(`\\`)353 case '\n':354 b.WriteString(`\n`)355 case '\r':356 b.WriteString(`\r`)357 case '\t':358 b.WriteString(`\t`)359 default:360 if r < 0x20 {361 b.WriteString(`\u00`)362 const hex = "0123456789abcdef"363 b.WriteByte(hex[(r>>4)&0xf])364 b.WriteByte(hex[r&0xf])365 continue366 }367 b.WriteRune(r)368 }369 }370 b.WriteString(`"`)371 return b.String()372}373Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.