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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

keys.gno

5.57 Kb · 167 lines
  1package config
  2
  3import (
  4	"strings"
  5
  6	"chain/runtime"
  7	"chain/runtime/unsafe"
  8)
  9
 10// The key grammar.
 11//
 12// A key is a NAME, optionally followed by "@" and a SCOPE:
 13//
 14//	pause                  applies to every realm that asks
 15//	pause@r/moul/home      applies to that realm only
 16//
 17// The scope is a package path with the chain domain stripped, because the
 18// domain is the same for every realm reading this one and repeating it in a
 19// few dozen keys buys nothing but storage.
 20//
 21// Two settings therefore answer every scoped question, and each reader decides
 22// how they combine: pause takes the stricter of the two (pausable.Strictest),
 23// while the notice blocks show both. That choice belongs to the reader and not
 24// here, because "stricter wins" and "show both" are both right, for different
 25// settings.
 26const (
 27	// ScopeSep separates a name from the realm it applies to. "@" and not "."
 28	// so a scope can never be mistaken for a longer name, and not ":" because
 29	// gno realm render paths already use that.
 30	ScopeSep = "@"
 31
 32	maxNameLen  = 64
 33	maxScopeLen = 128
 34)
 35
 36// realmPath is this realm's own path, including its version. Stated once so
 37// the page can link to itself and the proxy check can recognise a sibling
 38// version without either restating the string.
 39const realmPath = "gno.land/r/moul/config/v1"
 40
 41// validName reports whether name is a legal setting name: 1..maxNameLen bytes
 42// of [a-z0-9._-]. The dot is the namespace separator ("block.top"), lowercase
 43// only so a setting has exactly one name, and no whitespace so a name
 44// round-trips through Manifest's tab-separated lines.
 45func validName(name string) bool {
 46	if len(name) == 0 || len(name) > maxNameLen {
 47		return false
 48	}
 49	for i := 0; i < len(name); i++ {
 50		c := name[i]
 51		switch {
 52		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
 53		case c == '-', c == '_', c == '.':
 54		default:
 55			return false
 56		}
 57	}
 58	return true
 59}
 60
 61// validScope reports whether scope is shaped like a package path with the
 62// domain stripped: 1..maxScopeLen bytes of [a-z0-9._/-], no empty segment.
 63func validScope(scope string) bool {
 64	if len(scope) == 0 || len(scope) > maxScopeLen {
 65		return false
 66	}
 67	lastSlash := true // a leading slash would be an empty first segment
 68	for i := 0; i < len(scope); i++ {
 69		c := scope[i]
 70		switch {
 71		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
 72			lastSlash = false
 73		case c == '-', c == '_', c == '.':
 74			lastSlash = false
 75		case c == '/':
 76			if lastSlash {
 77				return false
 78			}
 79			lastSlash = true
 80		default:
 81			return false
 82		}
 83	}
 84	return !lastSlash
 85}
 86
 87// validKey reports whether key is a legal name, or a legal name and scope
 88// joined by ScopeSep.
 89func validKey(key string) bool {
 90	name, scope := SplitKey(key)
 91	if scope == "" {
 92		return !strings.Contains(key, ScopeSep) && validName(name)
 93	}
 94	return validName(name) && validScope(scope)
 95}
 96
 97// SplitKey takes a key apart. An unscoped key returns an empty scope, and so
 98// does a malformed one: callers pair this with validKey rather than trusting
 99// the split.
100func SplitKey(key string) (name, scope string) {
101	i := strings.Index(key, ScopeSep)
102	if i < 0 {
103		return key, ""
104	}
105	return key[:i], key[i+1:]
106}
107
108// Scope turns a package path into the scope half of a key, stripping the chain
109// domain: "gno.land/r/moul/home" and "r/moul/home" both give "r/moul/home".
110//
111// A path that is not shaped like one comes back empty, and every caller here
112// treats that as "no scope", falling back to the global setting rather than
113// inventing a key nobody can type.
114//
115// The chain domain is tried first and the literal "gno.land/" second, so a key
116// written on one chain still resolves on another whose domain differs. Twin of
117// mygnoscan.TrimDomain, which answers the same question for a URL and is
118// stricter about the characters, because its output lands inside a link.
119func Scope(pkgPath string) string {
120	s := strings.TrimPrefix(pkgPath, runtime.ChainDomain()+"/")
121	s = strings.TrimPrefix(s, "gno.land/")
122	s = strings.Trim(s, "/")
123	if !validScope(s) {
124		return ""
125	}
126	return s
127}
128
129// KeyFor builds the scoped key for a setting on one realm, and returns the
130// bare name when pkgPath names no realm this can scope to.
131//
132// This is what a realm should render when it wants to tell a manager which
133// setting to change, so the command in the page is the command that works.
134func KeyFor(name, pkgPath string) string {
135	scope := Scope(pkgPath)
136	if scope == "" {
137		return name
138	}
139	return name + ScopeSep + scope
140}
141
142// caller is the package path of the realm that called into this one.
143//
144// Every exported function here that uses it is a plain read with no `cur realm`
145// parameter, so it can only ever be BORROWED: gno runs it without opening a
146// realm frame, and unsafe.CurrentRealm() therefore reports the borrower. That
147// is what makes the zero-argument helpers (TopBlock, IsPaused, …) able to name
148// their caller at all.
149//
150// Measured in the test harness on 2026-09-22: a call from a code realm at
151// gno.land/r/test/caller reports CurrentRealm=gno.land/r/test/caller and
152// PreviousRealm=gno.land/r/moul/config/v1.
153//
154// The moment one of these grows a `cur realm` parameter this stops being true
155// and starts reporting this realm instead. Do not add one. Anything that needs
156// a realm frame takes the path explicitly, which is what the …For variants are.
157func caller() string { return unsafe.CurrentRealm().PkgPath() }
158
159// scopedPair reads both halves of a scoped setting: the global one and the one
160// for pkgPath, either of which may be empty.
161func scopedPair(name, pkgPath string) (global, scoped string) {
162	global = Get(name)
163	if scope := Scope(pkgPath); scope != "" {
164		scoped = Get(name + ScopeSep + scope)
165	}
166	return global, scoped
167}