diff --git a/ABOUT.md b/ABOUT.md new file mode 100644 index 0000000..9c86ebc --- /dev/null +++ b/ABOUT.md @@ -0,0 +1,117 @@ +# Opinionated Origins: Why Figtree Is Named the Way It Is + +## Words Matter + +Every package makes a choice about what to name things. Most developers treat +naming as cosmetic — a label on a box. Figtree treats naming as architectural. +The words you use to describe a system shape the system itself. They shape what +you build next, what feels natural to extend, and what feels like it doesn't +belong. + +Viper chose a snake. Cobra. Viper. The fangs-first family of Go configuration +tooling. A snake is flat. It has no branches. It moves in one direction. It +strikes. If you've ever been bitten by a race condition in a viper-backed +application under concurrent load, the metaphor lands harder than intended. + +Figtree chose a tree. Specifically, the fig tree — one of the oldest cultivated +plants in human history, appearing in more foundational texts than almost any +other living thing. Not because of religion. Because of what a fig tree actually +does. + +## What a Fig Tree Does + +A fig tree grows from a seed. It puts down roots that draw from the environment. +It branches. Its branches bear fruit. The fruit contains the value. You harvest +the fruit. You don't harvest the tree. + +That's the entire figtree API described in one paragraph. + +- `figtree.New()` or `figtree.Grow()` — plant the seed +- `figs.NewString()`, `figs.NewInt()` — register properties, grow the branches +- `figs.Parse()` or `figs.Load()` — draw from the environment +- `*figs.String()`, `*figs.Int()` — harvest the fruit +- `figs.Fig(key)` — access the fruit itself, not just its value +- `figs.Mutations()` — observe how the fruit changes over time +- `figs.Withered` — the original state before the environment changed it +- `figs.Resurrect()` — regrow from dormant roots +- `figs.Curse()` / `figs.Recall()` — dormancy and renewal +- `figs.Pollinate()` — external forces updating the living tree +- `figs.Branch()` — the tree literally branches, each branch its own tree + +None of these words were chosen arbitrarily. Each one maps to a real biological +process that has a direct analog in what the code does. When a word fits its +behavior this precisely, the API becomes memorable. You don't have to look up +what `Resurrect` does. You already know. + +## Why the Naming Convention Enables Functionality Viper Lacks + +This isn't just aesthetics. The tree metaphor opened design space that a flat +snake metaphor closes off. + +A snake is flat. Viper's configuration is a flat map with dot-notation keys +pretending to be hierarchy. `viper.Get("db.host")` isn't a branch — it's a +string with a period in it. The hierarchy is a convention, not a structure. You +can't put rules on `db`. You can't put validators on `db`. You can't watch +`db` change independently. You can't scope callbacks to `db`. Because `db` +doesn't exist. Only `"db.host"` exists, as a string key in a flat map. + +A tree branches. `figs.NewBranch("db")` returns a real figtree. That branch +has its own validators, its own callbacks, its own rules, its own aliases. It +funnels mutations back to the root channel with the path recorded. It can be +passed around, composed, and reasoned about independently. `db` exists because +a tree has branches and a snake does not. + +The naming convention didn't just describe the API. It generated the API. Once +you commit to the metaphor honestly, the next feature becomes obvious. Trees +have branches. Branches bear fruit. Fruit can be withered or fresh. The tree +can be cursed or recalled. Pollination brings external changes in. The language +tells you what to build next. + +## The Concurrent Application Problem + +Figtree has been backing highly concurrent Go applications in enterprise +environments for over six years — first as `configurable`, then as `figs`, now +as `figtree`. Not because viper was unavailable. Because viper's known race +conditions were unacceptable in production systems that couldn't afford to lock +an external mutex around every configuration read. + +A tree is alive. It doesn't stop growing because something is reading its fruit. +The concurrency model in figtree — `sync.RWMutex` used correctly throughout, +read operations using `RLock` not `Lock`, `Store()` releasing locks before +channel sends to prevent deadlock — these decisions came from six years of +running configuration management under real concurrent load. The metaphor of a +living tree that continues to operate while being observed isn't decorative. +It's the design requirement. + +## On Adoption + +Figtree was built by someone who was adopted. Viper was not something to be +adopted. The irony is precise and intentional. + +The packages you choose for your applications are not neutral. They carry the +assumptions of their authors. A package named after a predator assumes adversarial +relationships — between the config and the application, between defaults and +overrides, between what you asked for and what you got. A package named after a +living, branching, fruit-bearing tree assumes that configuration should grow +with your application, bear real value, and be observable as it changes. + +The words you use manifest. Choose them carefully. + +## On Yeshua + +Figtree isn't a religious package. It doesn't require belief in anything except +that good software is worth building carefully and naming honestly. + +But the fig tree appears in contexts most developers will recognize at some level +whether they identify as religious or not. It represents discernment — the +ability to look at something and know whether it bears fruit or doesn't. That +instinct is what every engineer uses when evaluating a dependency. + +Does this bear fruit? Does it do what it says? Is it alive or is it dead wood? + +Figtree bears fruit. That's the whole argument. + +--- + +*Figtree is maintained by [@andreimerlescu](https://github.com/andreimerlescu).* +*License: MIT* diff --git a/COMPARISON.md b/COMPARISON.md new file mode 100644 index 0000000..910726b --- /dev/null +++ b/COMPARISON.md @@ -0,0 +1,383 @@ +# Figtree vs Viper: A Practical Comparison + +This document is written for Go developers and engineering organizations evaluating +configuration management packages. It assumes you are **not** using remote configuration +sources — which represents the majority of real-world Go projects. + +--- + +## TL;DR + +| Capability | Viper | Figtree | +|---|---|---| +| CLI flags | ✅ (via pflag) | ✅ (via stdlib flag) | +| Environment variables | ✅ | ✅ | +| Config files (YAML/JSON/INI) | ✅ | ✅ | +| File watching | ✅ | 🔜 planned v2.1.1+ | +| Struct unmarshaling | ✅ | 🔜 planned v2.2.0+ | +| Per-property validators | ❌ | ✅ 36 built-in | +| Per-property callbacks | ❌ | ✅ | +| Mutation tracking channel | ❌ | ✅ | +| Property aliases | ⚠️ shallow | ✅ full propagation | +| Property rules | ❌ | ✅ | +| Struct tag validation (assure:) | ❌ | 🔜 planned v2.2.0+ | +| Organizational branches | ❌ | 🔜 planned v2.3.0+ | +| Known race conditions | ⚠️ yes | ✅ AI Battle Tested | +| Remote config sources | ✅ | 🔜 planned | +| stdlib flag compatibility | ❌ | ✅ | +| Zero dependencies (core) | ❌ | ✅ | + +--- + +## Philosophy + +**Viper** was designed to be the swiss army knife of Go configuration. It integrates +with `pflag`, supports remote backends like etcd and Consul, and has accumulated years +of community contributions. Its breadth is its strength and its weakness — the API +surface is large, the concurrency model has known issues, and per-property behavior +is not possible without wrapping viper yourself. + +**Figtree** was designed around a single premise: every configurable property in your +application is a first-class citizen with its own validators, callbacks, aliases, and +rules. The tree is the unit of organization. Properties are the unit of behavior. +Mutations are observable. The API is opinionated so your application does not have to be. + +--- + +## Feature Breakdown + +### CLI Flags + +Viper delegates flag parsing to `pflag`, a POSIX-compliant flag package that is itself +a dependency. Figtree uses the Go standard library `flag` package directly, introducing +zero additional dependencies for flag parsing. + +```go +// Viper (requires pflag) +pflag.String("host", "localhost", "database host") +viper.BindPFlag("host", pflag.Lookup("host")) + +// Figtree +figs.NewString("host", "localhost", "database host") +``` + +### Environment Variables + +Both packages support environment variable binding. Figtree resolves environment +variables automatically by uppercasing the property name and does not require explicit +binding calls. + +```go +// Viper +viper.SetEnvPrefix("APP") +viper.AutomaticEnv() +viper.BindEnv("host") + +// Figtree — automatic, no binding required +figs.NewString("host", "localhost", "database host") +// HOST=db.example.com ./myapp works automatically +``` + +### Config Files + +Both packages support YAML, JSON, and INI formats. Figtree resolves the config file +path through a priority chain: `CONFIG_FILE` environment variable, `Options.ConfigFile`, +package-level `ConfigFilePath`, then conventional filenames in the working directory. + +```go +// Viper +viper.SetConfigName("config") +viper.SetConfigType("yaml") +viper.AddConfigPath(".") +viper.ReadInConfig() + +// Figtree +figs := figtree.With(figtree.Options{ConfigFile: "config.yaml"}) +figs.Load() +``` + +### File Watching + +Viper provides `viper.WatchConfig()` with an `OnConfigChange` callback. Figtree provides +`Options{Watch: true}` with mutations flowing through the existing `Mutations()` channel +— no separate callback registration required. File-driven changes flow through the same +validators, callbacks, and rules as programmatic changes. + +```go +// Viper +viper.WatchConfig() +viper.OnConfigChange(func(e fsnotify.Event) { + // raw event, no validation, no type safety +}) + +// Figtree +figs := figtree.With(figtree.Options{ + Watch: true, + Tracking: true, + ConfigFile: "config.yaml", +}) +figs.Load() +for mutation := range figs.Mutations() { + log.Printf("%s changed: %v → %v", mutation.Property, mutation.Old, mutation.New) +} +``` + +### Per-Property Validators + +Viper has no built-in validation. Developers must validate values after retrieval, +scattering validation logic across the codebase. Figtree ships 36 built-in validators +and supports custom `func(interface{}) error` validators registered per property. + +```go +// Viper — validation is your problem +host := viper.GetString("host") +if host == "" { + return errors.New("host is required") +} + +// Figtree — validation is declared at registration +figs.NewString("host", "", "database host") +figs.WithValidator("host", figtree.AssureStringNotEmpty) +figs.WithValidator("host", figtree.AssureStringHasPrefix("postgres://")) +// Parse() or Load() returns error if validation fails +``` + +The full validator table covers strings, booleans, integers, int64, float64, durations, +lists, and maps. See the [Available Validators](README.md#available-validators) section +for the complete reference. + +### Per-Property Callbacks + +Viper has no per-property callback system. Figtree supports three callback hooks per +property: `CallbackAfterVerify` (on Parse/Load), `CallbackAfterRead` (on every getter +call), and `CallbackAfterChange` (on every Store call and file-driven reload). + +```go +// Viper — no equivalent + +// Figtree +figs.WithCallback("host", figtree.CallbackAfterChange, func(value interface{}) error { + log.Printf("host changed to %v — reconnecting", value) + return reconnect(value.(string)) +}) +``` + +### Mutation Tracking + +Viper provides a single `OnConfigChange` hook for file-driven changes only. Programmatic +changes via `viper.Set()` produce no observable event. Figtree provides a buffered +channel that receives a `Mutation` for every value change regardless of source — file, +environment variable, flag, or programmatic `Store()` call. + +```go +// Viper — only file changes, no channel +viper.OnConfigChange(func(e fsnotify.Event) { ... }) + +// Figtree — all changes, channel-based, select-compatible +figs := figtree.With(figtree.Options{Tracking: true, Harvest: 100}) +go func() { + for mutation := range figs.Mutations() { + log.Printf("%s: %v → %v at %s", + mutation.Property, mutation.Old, mutation.New, mutation.When) + } +}() +``` + +### Property Aliases + +Viper supports `viper.RegisterAlias("host", "h")` but the alias only applies at the +getter level. Validators, callbacks, and setters do not propagate through aliases. +Figtree aliases are full citizens — everything that works on the canonical name works +identically on the alias. + +```go +// Viper — alias is getter-only +viper.RegisterAlias("verbose", "v") +// viper.Set("v", true) does NOT trigger OnConfigChange for "verbose" + +// Figtree — alias propagates everywhere +figs.NewBool("verbose", false, "enable verbose output") +figs.WithAlias("verbose", "v") +figs.WithValidator("verbose", figtree.AssureBoolTrue) +figs.StoreString("v", "true") // validator fires +*figs.Bool("verbose") // true +*figs.Bool("v") // true +``` + +### Property Rules + +Viper has no concept of property-level rules. Figtree provides rules that govern how +a property behaves at runtime, applied per-property or tree-wide. + +```go +// Figtree rules — no Viper equivalent +figs.WithRule("db-password", figtree.RulePreventChange) // immutable after Parse +figs.WithRule("debug", figtree.RulePanicOnChange) // panic on change +figs.WithTreeRule(figtree.RuleNoFlags) // disable all CLI flags +figs.WithTreeRule(figtree.RuleNoEnv) // disable all env vars +``` + +Full rule reference: + +| Rule | Behavior | +|---|---| +| `RulePreventChange` | Blocks all Store calls after initial value is set | +| `RulePanicOnChange` | Panics on any Store call | +| `RuleNoValidations` | Skips all WithValidator assignments | +| `RuleNoCallbacks` | Skips all WithCallback assignments | +| `RuleNoFlags` | Disables CLI flag parsing for the tree | +| `RuleNoEnv` | Skips all os.Getenv logic | +| `RuleNoMaps` | Blocks NewMap, StoreMap, and Map | +| `RuleNoLists` | Blocks NewList, StoreList, and List | +| `RuleCondemnedFromResurrection` | Panics on Resurrect attempts | + +### Struct Unmarshaling with Validation + +Viper provides `viper.Unmarshal(&cfg)` which populates a struct from the config store. +It has no validation layer — you get the values, validation is your responsibility. +Figtree's `Unmarshal` populates structs and runs inline validators declared via the +`assure:` struct tag, covering all 36 built-in validators. + +```go +// Viper +var cfg Config +viper.Unmarshal(&cfg) +// validate cfg yourself + +// Figtree +type DatabaseConfig struct { + Host string `fig:"host" assure:"notEmpty|hasPrefix=postgres://"` + Port int `fig:"port" assure:"inRange=1024,65535"` + Timeout time.Duration `fig:"timeout" assure:"min=5s|max=2m"` +} +var cfg DatabaseConfig +err := figs.Unmarshal(&cfg) +// validation runs inline, UnmarshalError carries field, fig key, and failing token +``` + +### Concurrency + +Viper has well-documented race conditions that have been open issues for years. Safe +concurrent use of viper requires external locking that the developer must implement +and maintain. Figtree was designed with concurrency as a first-class concern — all +internal operations use `sync.RWMutex` correctly, the mutations channel is safely +buffered, and `Store()` releases locks before channel sends to prevent deadlock. + +--- + +## Migration from Viper to Figtree + +The conceptual mapping is straightforward for projects not using remote config sources. + +### Installation + +```bash +go get -u github.com/andreimerlescu/figtree/v2 +``` + +### Import + +```go +// Before +import "github.com/spf13/viper" + +// After +import "github.com/andreimerlescu/figtree/v2" +``` + +### Initialization + +```go +// Viper +viper.SetDefault("host", "localhost") +viper.SetDefault("port", 5432) +viper.AutomaticEnv() + +// Figtree +figs := figtree.Grow() +figs.NewString("host", "localhost", "database host") +figs.NewInt("port", 5432, "database port") +``` + +### Reading Values + +```go +// Viper +host := viper.GetString("host") +port := viper.GetInt("port") + +// Figtree +host := *figs.String("host") +port := *figs.Int("port") +``` + +### Writing Values + +```go +// Viper +viper.Set("host", "db.example.com") + +// Figtree +figs.StoreString("host", "db.example.com") +``` + +### Config File Loading + +```go +// Viper +viper.SetConfigFile("config.yaml") +viper.ReadInConfig() + +// Figtree +figs := figtree.With(figtree.Options{ConfigFile: "config.yaml"}) +figs.Load() +``` + +### Struct Unmarshaling + +```go +// Viper +var cfg Config +viper.Unmarshal(&cfg) + +// Figtree +var cfg Config +figs.Unmarshal(&cfg) +``` + +--- + +## When to Choose Viper + +- Your project depends on `pflag` and you cannot migrate +- You require remote configuration sources (etcd, Consul, Vault) today +- You have deep existing viper integration with significant migration cost + +## When to Choose Figtree + +- You want per-property validation without writing it yourself +- You want observable mutations via a channel +- You want immutability rules on specific properties +- You want struct unmarshaling with inline validation +- You want a concurrency-safe configuration package +- You are not using viper's remote configuration capabilities +- You want zero non-stdlib dependencies in your configuration layer +- You want organizational structure via branches (v2.3.0+) + +--- + +## Summary + +For the majority of Go projects that use viper for flags, environment variables, and +config files — and nothing more — figtree offers a more expressive, safer, and more +maintainable alternative. The per-property model gives you validation, callbacks, rules, +and mutation tracking that viper requires you to build yourself. + +If your project does not use viper's remote configuration sources, figtree is worth +evaluating as a direct replacement. + +--- + +*Figtree is maintained by [@andreimerlescu](https://github.com/andreimerlescu).* +*Current stable release: v2.1.0* +*License: MIT* diff --git a/README.md b/README.md index 8d68a59..17bcb85 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Fig Tree -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +Figtree logo created by xAI Grok Fig Tree is a command line utility configuration manager that you can refer to as `figs`, as you conFIGure your -application's runtime. +application's runtime. Check out [ABOUT.md](/ABOUT.md) if you want to learn about the origins of the package itself and why I built it. -![Figtree](/figtree.jpg "Figtree by xAI Grok 3") +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## Installation @@ -15,6 +15,34 @@ To use `figtree` in your project, `go get` it... go get -u github.com/andreimerlescu/figtree/v2 ``` +## Feature Comparison of Figtree vs Viper + +Figtree was designed to be a replacement for Viper since I prefer fruit over fangs of a snake and I am a programmer capable of investing the energy into giving you an alternative that packs a strong punch. + +Think of it this way: _Viper_ is for Eve and _Figtree_ is for Adam. + +| Capability | Viper | Figtree | +|---|---|---| +| CLI flags | ✅ (via pflag) | ✅ (via stdlib flag) | +| Environment variables | ✅ | ✅ | +| Config files (YAML/JSON/INI) | ✅ | ✅ | +| File watching | ✅ | 🔜 planned v2.1.1+ | +| Struct unmarshaling | ✅ | 🔜 planned v2.2.0+ | +| Per-property validators | ❌ | ✅ 36 built-in | +| Per-property callbacks | ❌ | ✅ | +| Mutation tracking channel | ❌ | ✅ | +| Property aliases | ⚠️ shallow | ✅ full propagation | +| Property rules | ❌ | ✅ | +| Struct tag validation (assure:) | ❌ | 🔜 planned v2.2.0+ | +| Organizational branches | ❌ | 🔜 planned v2.3.0+ | +| Known race conditions | ⚠️ yes | ✅ fixed | +| Remote config sources | ✅ | 🔜 planned | +| stdlib flag compatibility | ❌ | ✅ | +| Zero dependencies (core) | ❌ | ✅ | + +[Read full case study comparison...](/COMPARISON.md) + + ## Usage To use **figs** package in your Go code, you need to import it: @@ -266,6 +294,7 @@ With callbacks, you can really slow the performance down of `figtree`, but when At the end of the day, you'll know what's best to use. I build what I build because its the best that I use. + ### Complex Example Usage ```go diff --git a/logo.jpeg b/logo.jpeg new file mode 100644 index 0000000..7f36050 Binary files /dev/null and b/logo.jpeg differ