Objective
figs := figtree.Grow()
figs.NewSemaphore("max-sessions", 369, "maximum allowed sessions")
_ := figs.Parse()
*figs.Semaphore("max-sessions").Acquire()
*figs.Semaphore("max-sessions").Release()
figs.StoreSemaphore("max-sessions", 434)
// drains existing, closes existing, creates new
figs.WithValidator("max-sessions", figtree.AssureSemaphoreLessThan(1))
figs.WithValidator("max-sessions", figtree.AssureSemaphoreGreaterThan(2))
figs.WithValidator("max-sessions", figtree.AssureSemaphoreIs(3))
Additionally, from the ROADMAP.md there are notes for this functionality plus the file and directory functionality.
Updating Existing Files
In types.go the const () table should be updated to add tFile, tDirectory and tSemaphore.
const (
tString Mutagenesis = "tString"
tInt Mutagenesis = "tInt"
tInt64 Mutagenesis = "tInt64"
tFloat64 Mutagenesis = "tFloat64"
tDuration Mutagenesis = "tDuration"
tUnitDuration Mutagenesis = "tUnitDuration"
tBool Mutagenesis = "tBool"
tList Mutagenesis = "tList"
tMap Mutagenesis = "tMap"
tFile Mutagenesis = "tFile"
tDirectory Mutagenesis = "tDirectory"
tSemaphore Mutagenesis = "tSemaphore"
)
type Semaphoric interface {
// Semaphore returns the live *sema.Semaphore registered under name
Semaphore(name string) *sema.Semaphore
// NewSemaphore registers a new Semaphore fig with an initial capacity
NewSemaphore(name string, capacity int, usage string) Plant
// SemaphoreCapacity returns the current registered capacity integer
SemaphoreCapacity(name string) int
}
type Fileable interface {
File(name string) *string
NewFile(name, path, usage string) Plant
StoreFile(name, path string) Plant
FileContents(name string) ([]byte, error)
FileHandler(name string) (*os.File, error)
FileWriteContents(name string, contents []byte) error
}
type Directable interface {
Directory(name string) *string
NewDirectory(name, path, usage string) Plant
StoreDirectory(name, path string) Plant
DirectoryFlushAll(name string) error
}
type figTree struct {
// ... existing fields unchanged ...
semaphores sync.Map // map[string]*sema.Semaphore — live objects separate from capacity values
}
type Flesh interface {
// ... existing methods unchanged ...
IsFile() bool
IsDirectory() bool
IsSemaphore() bool
ToFile() string // same as ToString but signals intent
ToDirectory() string // same as ToString but signals intent
ToSemaphore() int // returns capacity integer
}
type CoreMutations interface {
Intable
Intable64
Floatable
String
Flaggable
Durable
Listable
Mappable
Fileable // new
Directable // new
Semaphoric // new
}
assure.go Updates
// AssureFileExists ensures the file at the registered path exists.
var AssureFileExists = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{Exists: true})
}
// AssureFileTouchIfNotExists creates the file if it does not exist.
var AssureFileTouchIfNotExists = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Create: file.Create{
Kind: file.IfNotExists,
OpenFlag: os.O_CREATE | os.O_WRONLY,
FileMode: 0644,
},
})
}
// AssureFileCanBeModified ensures the file at the path is writable.
var AssureFileCanBeModified = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
RequireWrite: true,
})
}
// AssureFileSizeGreaterThan ensures the file size exceeds the given byte count.
var AssureFileSizeGreaterThan = func(size int64) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
IsGreaterThan: size,
})
}
}
// AssureFileSizeLessThan ensures the file size is below the given byte count.
var AssureFileSizeLessThan = func(size int64) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
IsLessThan: size,
})
}
}
// AssureFileModeIs ensures the file has exactly the given os.FileMode.
var AssureFileModeIs = func(mode os.FileMode) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
IsFileMode: mode,
})
}
}
// AssureFileOwnerIs ensures the file is owned by the given user (UID as string).
var AssureFileOwnerIs = func(owner string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
RequireOwner: owner,
})
}
}
// AssureFileGroupIs ensures the file belongs to the given group (GID as string).
var AssureFileGroupIs = func(group string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
return check.File(v.ToString(), file.Options{
Exists: true,
RequireGroup: group,
})
}
}
// - directory validators
// AssureDirExists ensures the directory at the registered path exists.
var AssureDirExists = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{Exists: true})
}
// AssureDirCreateIfNotExists creates the directory if it does not exist.
var AssureDirCreateIfNotExists = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Create: directory.Create{
Kind: directory.IfNotExists,
FileMode: 0755,
},
})
}
// AssureDirIsReadable ensures the directory is readable.
var AssureDirIsReadable = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
MorePermissiveThan: 0444,
})
}
// AssureDirIsWritable ensures the directory is writable.
var AssureDirIsWritable = func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
RequireWrite: true,
})
}
// AssureDirOwnerIs ensures the directory is owned by the given user (UID as string).
var AssureDirOwnerIs = func(owner string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
RequireOwner: owner,
})
}
}
// AssureDirGroupIs ensures the directory belongs to the given group (GID as string).
var AssureDirGroupIs = func(group string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
RequireGroup: group,
})
}
}
// AssureDirChmod ensures the directory has exactly the given os.FileMode.
var AssureDirChmod = func(mode os.FileMode) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
// checkfs directory.Options doesn't have IsFileMode directly,
// so we bracket it with both MorePermissiveThan and LessPermissiveThan
return check.Directory(v.ToString(), directory.Options{
Exists: true,
MorePermissiveThan: mode,
LessPermissiveThan: mode,
})
}
}
// AssureDirMorePermissiveThan ensures the directory mode is at least as permissive as mode.
var AssureDirMorePermissiveThan = func(mode os.FileMode) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
MorePermissiveThan: mode,
})
}
}
// AssureDirLessPermissiveThan ensures the directory mode is no more permissive than mode.
var AssureDirLessPermissiveThan = func(mode os.FileMode) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
return check.Directory(v.ToString(), directory.Options{
Exists: true,
LessPermissiveThan: mode,
})
}
}
// - semaphore validators
// AssureSemaphoreGreaterThan ensures the registered semaphore capacity exceeds the given value.
var AssureSemaphoreGreaterThan = func(above int) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsSemaphore() {
return ErrInvalidType{tSemaphore, value}
}
capacity := v.ToSemaphore()
if capacity <= above {
return ErrValue{ErrWayBeAbove, capacity, above}
}
return nil
}
}
// AssureSemaphoreLessThan ensures the registered semaphore capacity is below the given value.
var AssureSemaphoreLessThan = func(below int) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsSemaphore() {
return ErrInvalidType{tSemaphore, value}
}
capacity := v.ToSemaphore()
if capacity >= below {
return ErrValue{ErrWayBeBelow, capacity, below}
}
return nil
}
}
// AssureSemaphoreIs ensures the registered semaphore capacity is exactly the given value.
var AssureSemaphoreIs = func(exact int) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsSemaphore() {
return ErrInvalidType{tSemaphore, value}
}
capacity := v.ToSemaphore()
if capacity != exact {
return fmt.Errorf("semaphore capacity must be exactly %d, got %d", exact, capacity)
}
return nil
}
}
conversions.go Updates
// toSemaphore returns an interface{} as an int capacity or returns an error.
// tSemaphore stores its capacity as an int — this is just a typed wrapper
// around toInt to make the call site in figFlesh.ToSemaphore() explicit.
func toSemaphore(value interface{}) (int, error) {
switch v := value.(type) {
case *Value:
return toSemaphore(v.Value)
case *figFlesh:
return toSemaphore(v.AsIs())
default:
i, err := toInt(value)
if err != nil {
return 0, ErrConversion{MutagenesisOf(value), tSemaphore, v}
}
return i, nil
}
}
-
toInt needs a tSemaphore path.
Since tSemaphore stores its capacity as an int underneath, and validators call v.ToSemaphore() which will ultimately need to extract that int, you need the conversion chain to work. But actually ToSemaphore() on figFlesh would just call toInt() internally — and toInt() already handles int, *int, string, *string etc. So no change needed there, it falls through naturally.
-
toString needs tFile and tDirectory cases.
These are just strings underneath, so toString already handles them via the *string and string cases. No change needed.
flesh.go Updates
func (flesh *figFlesh) Is(mutagenesis Mutagenesis) bool {
switch mutagenesis {
case tInt:
return flesh.IsInt()
case tInt64:
return flesh.IsInt64()
case tFloat64:
return flesh.IsFloat64()
case tBool:
return flesh.IsBool()
case tString:
return flesh.IsString()
case tList:
return flesh.IsList()
case tMap:
return flesh.IsMap()
case tDuration:
return flesh.IsDuration()
case tUnitDuration:
return flesh.IsUnitDuration()
case tFile: // new
return flesh.IsFile()
case tDirectory: // new
return flesh.IsDirectory()
case tSemaphore: // new
return flesh.IsSemaphore()
default:
return false
}
}
func (flesh *figFlesh) IsFile() bool {
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsFile()
case string:
return true
case *string:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) IsDirectory() bool {
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsDirectory()
case string:
return true
case *string:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) IsSemaphore() bool {
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsSemaphore()
case int:
return true
case *int:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) ToSemaphore() int {
f, e := toSemaphore(flesh.Flesh)
if e != nil {
isFlesh, ok := flesh.Flesh.(*figFlesh)
if ok {
f, e = toSemaphore(isFlesh.Flesh)
if e != nil {
return 0
}
}
}
return f
}
mutations_new.go Updates
// NewFile registers a new file path property
func (tree *figTree) NewFile(name, path, usage string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = strings.ToLower(name)
if _, exists := tree.figs[name]; exists {
tree.problems = append(tree.problems, fmt.Errorf("name '%s' already exists", name))
return tree
}
tree.activateFlagSet()
vPtr := &Value{
Value: path,
Mutagensis: tFile,
}
tree.values.Store(name, vPtr)
tree.flagSet.Var(vPtr, name, usage)
def := &figFruit{
name: name,
usage: usage,
Mutagenesis: tFile,
Mutations: make([]Mutation, 0),
Validators: make([]FigValidatorFunc, 0),
Callbacks: make([]Callback, 0),
Rules: make([]RuleKind, 0),
}
tree.figs[name] = def
if _, exists := tree.withered[name]; !exists {
tree.withered[name] = witheredFig{
name: name,
Value: *vPtr,
Mutagenesis: tFile,
}
}
return tree
}
// NewDirectory registers a new directory path property
func (tree *figTree) NewDirectory(name, path, usage string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = strings.ToLower(name)
if _, exists := tree.figs[name]; exists {
tree.problems = append(tree.problems, fmt.Errorf("name '%s' already exists", name))
return tree
}
tree.activateFlagSet()
vPtr := &Value{
Value: path,
Mutagensis: tDirectory,
}
tree.values.Store(name, vPtr)
tree.flagSet.Var(vPtr, name, usage)
def := &figFruit{
name: name,
usage: usage,
Mutagenesis: tDirectory,
Mutations: make([]Mutation, 0),
Validators: make([]FigValidatorFunc, 0),
Callbacks: make([]Callback, 0),
Rules: make([]RuleKind, 0),
}
tree.figs[name] = def
if _, exists := tree.withered[name]; !exists {
tree.withered[name] = witheredFig{
name: name,
Value: *vPtr,
Mutagenesis: tDirectory,
}
}
return tree
}
// NewSemaphore registers a new immutable semaphore with a fixed capacity.
// Semaphores are not CLI flags and cannot be changed after registration.
func (tree *figTree) NewSemaphore(name string, capacity int, usage string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = strings.ToLower(name)
if _, exists := tree.figs[name]; exists {
tree.problems = append(tree.problems, fmt.Errorf("name '%s' already exists", name))
return tree
}
// capacity sanity check — sema.New clamps to 1 minimum,
// but we surface the problem at registration time instead
if capacity < 1 {
tree.problems = append(tree.problems, fmt.Errorf(
"semaphore '%s' capacity must be at least 1, got %d", name, capacity,
))
return tree
}
v := &Value{
Value: capacity,
Mutagensis: tSemaphore,
}
tree.values.Store(name, v)
// Note: no flagSet.Var — semaphores are not CLI configurable
def := &figFruit{
name: name,
usage: usage,
Mutagenesis: tSemaphore,
Mutations: make([]Mutation, 0),
Validators: make([]FigValidatorFunc, 0),
Callbacks: make([]Callback, 0),
Rules: []RuleKind{RulePreventChange}, // immutable by design
}
tree.figs[name] = def
if _, exists := tree.withered[name]; !exists {
tree.withered[name] = witheredFig{
name: name,
Value: *v,
Mutagenesis: tSemaphore,
}
}
// Create the live semaphore immediately — capacity is validated
// at Parse()/Load() time via WithValidator, but the object exists now
tree.semaphores.Store(name, sema.New(capacity))
return tree
}
// File returns the registered file path
func (tree *figTree) File(name string) *string {
return tree.String(name) // delegates cleanly — same storage, same pipeline
}
// Directory returns the registered directory path
func (tree *figTree) Directory(name string) *string {
return tree.String(name) // delegates cleanly — same storage, same pipeline
}
// Semaphore returns the live *sema.Semaphore registered under name.
// Returns nil if the name is not registered or not a tSemaphore fig.
func (tree *figTree) Semaphore(name string) sema.Semaphore {
tree.mu.RLock()
defer tree.mu.RUnlock()
name = strings.ToLower(tree.resolveName(name))
fruit, ok := tree.figs[name]
if !ok || fruit == nil || fruit.Mutagenesis != tSemaphore {
return nil
}
err := fruit.runCallbacks(tree, CallbackBeforeRead)
if err != nil {
fruit.Error = errors.Join(fruit.Error, err)
tree.figs[name] = fruit
return nil
}
s, ok := tree.semaphores.Load(name)
if !ok {
return nil
}
err = fruit.runCallbacks(tree, CallbackAfterRead)
if err != nil {
fruit.Error = errors.Join(fruit.Error, err)
tree.figs[name] = fruit
return nil
}
return s.(sema.Semaphore)
}
// SemaphoreCapacity returns the registered capacity integer for a semaphore fig.
func (tree *figTree) SemaphoreCapacity(name string) int {
tree.mu.RLock()
defer tree.mu.RUnlock()
name = strings.ToLower(tree.resolveName(name))
fruit, ok := tree.figs[name]
if !ok || fruit == nil || fruit.Mutagenesis != tSemaphore {
return 0
}
value, err := tree.from(name)
if err != nil {
return 0
}
i, err := toInt(value.Value)
if err != nil {
return 0
}
return i
}
Update mutations.go
func (tree *figTree) Store(mut Mutagenesis, name string, value interface{}) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = tree.resolveName(name)
fruit, ok := tree.figs[name]
if !ok || fruit == nil {
return tree
}
// tSemaphore is immutable by design — reject all Store attempts explicitly
if fruit.Mutagenesis == tSemaphore {
tree.figs[name].Error = errors.Join(
tree.figs[name].Error,
fmt.Errorf("semaphore '%s' is immutable and cannot be stored into", name),
)
return tree
}
// ... rest of Store unchanged
Update persist() method
persist needs two new cases — tFile and tDirectory both delegate to the tString case since storage is identical. Adding them explicitly prevents them from falling through to default and silently returning false:
case tFile:
// tFile stores a path string — identical pipeline to tString
old, err := toString(flesh)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, flesh, value
}
current, err := toString(value)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, old, value
}
valueAny, ok := tree.values.Load(name)
if !ok {
return false, flesh, value
}
v, ok := valueAny.(*Value)
if !ok {
return false, flesh, value
}
err = v.Assign(current)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, old, value
}
tree.values.Store(name, v)
tree.figs[name] = fruit
return !strings.EqualFold(old, current), old, current
case tDirectory:
// tDirectory stores a path string — identical pipeline to tString
old, err := toString(flesh)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, flesh, value
}
current, err := toString(value)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, old, value
}
valueAny, ok := tree.values.Load(name)
if !ok {
return false, flesh, value
}
v, ok := valueAny.(*Value)
if !ok {
return false, flesh, value
}
err = v.Assign(current)
if err != nil {
tree.figs[name].Error = errors.Join(tree.figs[name].Error, err)
return false, old, value
}
tree.values.Store(name, v)
tree.figs[name] = fruit
return !strings.EqualFold(old, current), old, current
// tSemaphore never reaches persist — blocked in Store() above
validators.go Updates
func (tree *figTree) validateAll() error {
tree.mu.RLock()
defer tree.mu.RUnlock()
err := tree.runCallbacks(CallbackBeforeVerify)
if err != nil {
return err
}
for name, fruit := range tree.figs {
if fruit.Error != nil {
return fruit.Error
}
if fruit.HasRule(RuleNoValidations) {
continue
}
for _, validator := range fruit.Validators {
if fruit != nil && validator != nil {
var val interface{}
_value := tree.useValue(tree.from(name))
if _value == nil {
fmt.Printf("skipping invalid fig '%s'\n", name)
continue
}
switch v := _value.Value.(type) {
case int:
val = v
case *int:
val = *v
case int64:
val = v
case *int64:
val = *v
case float64:
val = v
case *float64:
val = *v
case string:
val = v
case *string:
val = *v
case bool:
val = v
case *bool:
val = *v
case time.Duration:
val = v
case *time.Duration:
val = *v
case []string:
val = v
case *[]string:
val = *v
case map[string]string:
val = v
case *map[string]string:
val = *v
case ListFlag:
val = v.values
case *ListFlag:
val = v.values
case MapFlag:
val = v.values
case *MapFlag:
val = v.values
case Value:
val = v.Value
case *Value:
val = v.Value
default:
log.Printf("unknown fig type: %T for %v\n", v, v)
}
if val == nil {
log.Printf("val is nil for %s", name)
}
// Wrap val in mutagenesis context so assure funcs
// can distinguish tFile/tDirectory from tString
wrappedVal := &typedValue{
mutagenesis: fruit.Mutagenesis,
value: val,
}
if err := validator(wrappedVal); err != nil {
return fmt.Errorf("validation failed for %s: %v", name, err)
}
}
}
}
for _, fruit := range tree.figs {
if fruit.Error != nil {
return fruit.Error
}
}
return tree.runCallbacks(CallbackAfterVerify)
}
flesh.go
func (flesh *figFlesh) IsFile() bool {
if flesh.mutagenesis == tFile {
return true
}
// fallback for direct construction without mutagenesis context
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsFile()
case string:
return true
case *string:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) IsDirectory() bool {
if flesh.mutagenesis == tDirectory {
return true
}
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsDirectory()
case string:
return true
case *string:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) IsSemaphore() bool {
if flesh.mutagenesis == tSemaphore {
return true
}
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsSemaphore()
case int:
return true
case *int:
return f != nil
default:
return false
}
}
func (flesh *figFlesh) IsString() bool {
// if mutagenesis context is set, only match tString
if flesh.mutagenesis != "" && flesh.mutagenesis != tString {
return false
}
switch f := flesh.Flesh.(type) {
case *figFlesh:
return f.IsString()
case string:
return true
case *string:
return f != nil
default:
return false
}
}
validators.go updates
func (tree *figTree) WithValidator(name string, validator func(interface{}) error) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = tree.resolveName(name)
fig, ok := tree.figs[name]
if !ok {
tree.problems = append(tree.problems,
fmt.Errorf("WithValidator: fig '%s' not registered", name))
return tree
}
if fig.HasRule(RuleNoValidations) {
return tree
}
// tSemaphore validators must not be registered against non-semaphore figs
// and vice versa — catch obvious mismatches at registration time
// This is a best-effort check; we can't introspect the validator func's
// intended type without running it, so we rely on naming conventions
// (AssureFile*, AssureDir*, AssureSemaphore*) being used correctly.
// The mutagenesis field on figFlesh during validateAll is the real guard.
if fig.Validators == nil {
fig.Validators = make([]FigValidatorFunc, 0)
}
fig.Validators = append(fig.Validators, validator)
tree.figs[name] = fig
return tree
}
// makeFileValidator creates a validator for tFile path checks.
func makeFileValidator(check func(string) bool, errFormat string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{Flesh: value}
if !v.IsFile() {
return ErrInvalidType{tFile, value}
}
s := v.ToString()
if !check(s) {
return fmt.Errorf(errFormat, s)
}
return nil
}
}
// makeDirectoryValidator creates a validator for tDirectory path checks.
func makeDirectoryValidator(check func(string) bool, errFormat string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{Flesh: value}
if !v.IsDirectory() {
return ErrInvalidType{tDirectory, value}
}
s := v.ToString()
if !check(s) {
return fmt.Errorf(errFormat, s)
}
return nil
}
}
// makeSemaphoreValidator creates a validator for tSemaphore capacity checks.
func makeSemaphoreValidator(check func(int) bool, errFormat string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{Flesh: value}
if !v.IsSemaphore() {
return ErrInvalidType{tSemaphore, value}
}
i := v.ToSemaphore()
if !check(i) {
return fmt.Errorf(errFormat, i)
}
return nil
}
}
rules.go updates
const (
RuleUndefined RuleKind = iota
RulePreventChange RuleKind = iota
RulePanicOnChange RuleKind = iota
RuleNoValidations RuleKind = iota
RuleNoCallbacks RuleKind = iota
RuleCondemnedFromResurrection RuleKind = iota
RuleNoMaps RuleKind = iota
RuleNoLists RuleKind = iota
RuleNoFlags RuleKind = iota
RuleNoEnv RuleKind = iota
RuleNoFiles RuleKind = iota // blocks NewFile, StoreFile, File from being called on the Tree
RuleNoDirectories RuleKind = iota // blocks NewDirectory, StoreDirectory, Directory from being called on the Tree
)
mutations_new.go updates
func (tree *figTree) NewFile(name, path, usage string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
if tree.HasRule(RuleNoFiles) {
return tree
}
// ... rest unchanged
}
func (tree *figTree) NewDirectory(name, path, usage string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
if tree.HasRule(RuleNoDirectories) {
return tree
}
// ... rest unchanged
}
func (tree *figTree) WithRule(name string, rule RuleKind) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = tree.resolveName(name)
fruit, exists := tree.figs[name]
if !exists || fruit == nil {
return tree
}
// tSemaphore figs have RulePreventChange baked in at registration.
// Any attempt to add RuleNoValidations is also blocked — validators
// are the only mechanism to enforce capacity constraints at Parse time,
// and silently disabling them on an immutable type would be confusing.
if fruit.Mutagenesis == tSemaphore {
switch rule {
case RuleNoValidations, RuleNoCallbacks:
// these are permissible — callbacks are fine to suppress
// validations are debatable but we allow the caller to decide
default:
// for anything that would affect mutability, record a problem
// rather than silently accepting it
if rule == RulePreventChange || rule == RulePanicOnChange {
tree.problems = append(tree.problems, fmt.Errorf(
"WithRule: '%s' is redundant on semaphore '%s' — already immutable",
rule, name,
))
return tree
}
}
}
fruit.Rules = append(fruit.Rules, rule)
tree.figs[name] = fruit
return tree
}
func (r RuleKind) String() string {
switch r {
case RuleUndefined:
return "RuleUndefined"
case RulePreventChange:
return "RulePreventChange"
case RulePanicOnChange:
return "RulePanicOnChange"
case RuleNoValidations:
return "RuleNoValidations"
case RuleNoCallbacks:
return "RuleNoCallbacks"
case RuleCondemnedFromResurrection:
return "RuleCondemnedFromResurrection"
case RuleNoMaps:
return "RuleNoMaps"
case RuleNoLists:
return "RuleNoLists"
case RuleNoFlags:
return "RuleNoFlags"
case RuleNoEnv:
return "RuleNoEnv"
case RuleNoFiles:
return "RuleNoFiles"
case RuleNoDirectories:
return "RuleNoDirectories"
default:
return fmt.Sprintf("RuleKind(%d)", int(r))
}
}
fruit.go updates
func (v *Value) Set(in string) error {
switch v.Mutagensis {
case tString:
v.Value = in
case tFile:
// path stored as string — identical to tString
v.Value = in
case tDirectory:
// path stored as string — identical to tString
v.Value = in
case tSemaphore:
// semaphores are immutable — Set() must never succeed
// This guards against the flag package or file reload
// attempting to overwrite the capacity via the Value interface
v.Err = fmt.Errorf("semaphore capacity is immutable and cannot be set via string")
return v.Err
case tBool:
// ... existing unchanged
}
func (v *Value) Flesh() Flesh {
return &figFlesh{
Flesh: v.Value,
mutagenesis: v.Mutagensis,
}
}
func (v *Value) String() string {
return (&figFlesh{
Flesh: v.Value,
mutagenesis: v.Mutagensis,
}).ToString()
}
Objective
Additionally, from the ROADMAP.md there are notes for this functionality plus the file and directory functionality.
Updating Existing Files
In
types.gotheconst ()table should be updated to addtFile,tDirectoryandtSemaphore.assure.goUpdatesconversions.goUpdatestoInt needs a tSemaphore path.
Since tSemaphore stores its capacity as an int underneath, and validators call v.ToSemaphore() which will ultimately need to extract that int, you need the conversion chain to work. But actually ToSemaphore() on figFlesh would just call toInt() internally — and toInt() already handles int, *int, string, *string etc. So no change needed there, it falls through naturally.
toString needs tFile and tDirectory cases.
These are just strings underneath, so toString already handles them via the *string and string cases. No change needed.
flesh.goUpdatesmutations_new.goUpdatesUpdate
mutations.goUpdate
persist()methodpersist needs two new cases — tFile and tDirectory both delegate to the tString case since storage is identical. Adding them explicitly prevents them from falling through to default and silently returning false:
validators.goUpdatesflesh.govalidators.goupdatesrules.goupdatesmutations_new.goupdatesfruit.goupdates