Skip to content

Add Mutagenesis for tFile, tDirectory, and tSemaphore to the Figtree #22

@andreimerlescu

Description

@andreimerlescu

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()
}

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions