Skip to content

RealHarshThakur/c4

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

C4

Config schemas, validation, docs & LSP tooling β€” all from a single explicit API.

Most tools today ship YAML or TOML configuration with no schema, no validation, and no LSP, leaving users to guess structure and discover errors only in production. Misconfigurations lead to operational outages and security incidents.

Good configuration DX needs:

βœ” Clear documentation

βœ” Reliable validation

βœ” Editor feedback (hover, completion & diagnostics)

C4 provides all of this by letting you define configuration intent explicitly in Go β€” then generating everything else from that definition.


πŸ’‘ What C4 Does

You declare schema using a builder API (similar in spirit to Cobra flags):

func (c *Server) ConfigSchema(b *c4.SchemaBuilder) {
    b.Field(&c.Port).
        Default(8080).
        GreaterThan(0).
        LessThan(65535)
}

From this one definition, C4 generates:

  • a schema AST
  • CEL validation rules
  • defaults & examples
  • Markdown documentation
  • a full LSP (hover, completion, diagnostics)

Define intent once; C4 handles the rest.


πŸ›‘οΈ Why Validate & Document Early

Most config failures are preventable if the schema, rules, and examples are defined early, close to the code. C4 makes this trivial: once you declare fields and rules, it automatically produces validation, documentation, examples, and LSP diagnostics.

This also makes configuration agent-friendly β€” coding agents and MCP servers can auto-fill or fix configs because the schema is machine-readable.

And because docs and validation come directly from your schema, you can’t end up shipping a config surface without shipping its docs.

🧱 Why C4?

Viper/Koanf

Great at loading YAML/TOML β†’ structs(i.e schema-on-read), but they don’t provide:

  • schema & validation
  • generate config documentation
  • LSP support

When we expose configuration to users, it deserves the same level of intent and structure that we apply when designing a database schema.

Schema DSLs (CUE, Dhall, Pkl, JSONSchema)

They solve two things:

  1. Structure
  2. Validation

But they require:

  • learning a separate language
  • maintaining schema outside Go
  • keeping two sources of truth in sync

Go structs already define structure. C4 adds rules, docs, and examples through an explicit builder API β€” no DSLs, no drift, no duplication.

C4: the pragmatic middle path

  • Everything stays in Go
  • Schema is explicit, readable, and greppable
  • Supports human-friendly data formats
  • Generates docs & LSP tooling automatically
  • No struct tags, no DSLs, no scaffolding
  • Perfect balance of control and convenience

βš™οΈ Why CEL?

CEL fits perfectly as the validation engine because it is:

  • simple
  • familiar (common in infra tooling & growing)
  • safe (sandboxed, no user-defined Go funcs)
  • precise
  • easy for users to read and reason about
  • works across data formats
  • works at runtime without generating code

C4’s validation layer is directly inspired by protovalidate from Buf β€” a beautifully clean, declarative approach where:

  • CEL is used for custom rules
  • the surrounding framework defines sensible built-ins
  • validation is entirely data-driven

C4 brings the same cleanliness to configuration that protovalidate brought to APIs.


🧩 Why YAML & TOML?

(and how C4 stays format-agnostic)

C4 currently supports:

  • YAML
  • TOML

This keeps adoption smooth: users work in formats they already understand.

C4’s schema and validation layers are fully format-agnostic, meaning:

  • additional formats could be added in the future
  • nothing in the core is tied to YAML/TOML parsing
  • but we intentionally stay focused on the two most common human-facing formats for now

🧩 Why not Tree-Sitter?

C4 uses hand-crafted parsers instead of Tree-Sitter because, as a library, we wanted to avoid the CGo dependency. Tree-Sitter is a powerful tool, but its C core adds complexity to distribution and cross-compilation. Our custom parsers are lightweight, focused, and entirely in Go β€” making C4 trivial to embed and ship as a library dependency.


🧩 Why not struct tags?

C4 intentionally avoids struct tags because:

β€’ Tags can’t express rich rules without compromising on readability

(e.g., conditional logic, CEL expressions, examples, descriptions)

β€’ Tags are tied to serialization formats

(e.g., yaml:"..."), which makes them a poor place to define schema semantics

β€’ Cobra showed the better pattern

Cobra defines flags explicitly through builder APIs β€” not tags. C4 applies the same principle to configuration fields.

Explicit > implicit. Clear > clever. Format-agnostic > format-tied.


πŸš€ Getting Started

See working examples in the examples/ directory:

  • basic/ β€” Simple config struct with validation and marshaling
  • lsp/ β€” Full LSP server with hover, completion, and diagnostics
  • nested/ β€” Nested configuration structures
  • toml-config/ β€” TOML format example

πŸ“– Complete Guide: Config Struct β†’ Unmarshal β†’ Docs β†’ LSP

This guide walks you through the full workflow: creating a configuration struct, unmarshaling config files, generating documentation, and hooking up an LSP server for editor support.

Step 1: Define Your Configuration Struct

Create a struct with yaml, json, and toml tags (for format flexibility):

package main

import "github.com/realharshthakur/c4"

type AppConfig struct {
    Name    string `yaml:"name" json:"name" toml:"name"`
    Port    int    `yaml:"port" json:"port" toml:"port"`
    Host    string `yaml:"host" json:"host" toml:"host"`
    Debug   bool   `yaml:"debug" json:"debug" toml:"debug"`
}

Step 2: Implement the Configurable Interface

Add a ConfigSchema method to your struct. This is where you define rules, defaults, and constraints using the fluent builder API:

func (c *AppConfig) ConfigSchema(b *c4.SchemaBuilder) {
    b.Description("Application server configuration")
    
    b.Field(&c.Name).
        Required().
        MinLength(1).
        MaxLength(50).
        Description("Application name")
    
    b.Field(&c.Port).
        Default(8080).
        GreaterThan(0).
        LessThan(65535).
        Description("Port number to listen on")
    
    b.Field(&c.Host).
        Default("localhost").
        Description("Host address to bind to")
    
    b.Field(&c.Debug).
        Default(false).
        Description("Enable debug mode for verbose logging")
}

Builder API methods:

  • .Required() β€” Field must be set (non-zero value)
  • .Default(value) β€” Default value if not provided
  • .Description(text) β€” Documentation text
  • .GreaterThan(n) / .LessThan(n) β€” Numeric bounds
  • .MinLength(n) / .MaxLength(n) β€” String/slice length
  • .Pattern(regex) β€” Regex validation for strings
  • .Enum(values...) β€” Restrict to allowed values
  • .ValidationRule(expr, message) β€” Custom CEL validation
  • .RequiredIf(celExpr) β€” Conditional requirement

Step 3: Unmarshal and Validate Configuration

c4.Unmarshal() automatically:

  1. Detects the format (YAML, JSON, or TOML)
  2. Parses the data into your struct
  3. Applies defaults
  4. Validates against your rules
func main() {
    // Load from file
    var cfg AppConfig
    if err := c4.UnmarshalFile("config.yaml", &cfg); err != nil {
        log.Fatalf("Config error: %v", err)
    }
    
    // Or load from bytes with auto-detection
    yamlData := []byte(`
name: "my-app"
port: 3000
debug: true
`)
    
    if err := c4.Unmarshal(yamlData, &cfg); err != nil {
        log.Fatalf("Config error: %v", err)
    }
    
    // cfg is now valid and ready to use
    fmt.Printf("Starting %s on %s:%d\n", cfg.Name, cfg.Host, cfg.Port)
}

Error Handling:

If validation fails, Unmarshal returns a ConfigError with detailed field-level messages:

if err := c4.Unmarshal(data, &cfg); err != nil {
    if cfgErr, ok := err.(*c4.ConfigError); ok {
        for _, e := range cfgErr.Errors {
            fmt.Printf("Field %s: %s\n", e.Path, e.Message)
        }
    }
}

Step 4: Generate Documentation

Auto-generate markdown documentation directly from your schema definition:

func generateDocs() {
    docs := c4.GenerateDocs(&AppConfig{})
    
    // Write to file
    if err := os.WriteFile("CONFIG.md", []byte(docs), 0644); err != nil {
        log.Fatal(err)
    }
}

Generated documentation includes:

  • Field descriptions
  • Type information
  • Default values
  • Validation rules in human-readable format
  • Example YAML config

You can also use the CLI tool to auto-discover all Configurable types:

go run github.com/realharshthakur/c4/cmd/c4-docs@latest

Step 5: Hook Up an LSP Server

An LSP (Language Server Protocol) server provides IDE features:

  • Hover β€” See field descriptions and validation rules
  • Completion β€” Auto-complete field names and valid enum values
  • Diagnostics β€” Real-time validation errors with squiggles

White-labeling: The LSP server is easily white-labeled and can be shipped as part of your CLI tool. You can also build a branded VS Code extension or vim plugin on top of it β€” the LSP server handles all the heavy lifting, so the editor extensions are simple wrappers around stdio.

Create the LSP Server

package main

import (
    "github.com/realharshthakur/c4/lsp"
)

func main() {
    // Define all config types the server should support
    configTypes := map[string]any{
        "AppConfig":     &AppConfig{},
        "DatabaseConfig": &DatabaseConfig{},
    }
    
    // Configure LSP options
    opts := lsp.Options{
        Name:       "Config Language Server",
        Version:    "1.0.0",
        LanguageID: "yaml",  // For VS Code
        FilePatterns: []string{
            "*.yaml",
            "*.yml",
            "*.toml",
        },
        HoverEnabled:       true,
        CompletionEnabled:  true,
        DiagnosticsEnabled: true,
    }
    
    // Start the server (reads from stdin, writes to stdout)
    if err := lsp.Run(configTypes, opts); err != nil {
        panic(err)
    }
}

Schema Detection in Config Files

The LSP server auto-detects which schema to use by looking for a comment at the top of your config file:

# AppConfig
name: my-app
port: 8080
debug: true

The comment # AppConfig tells the server to validate this file against the AppConfig schema. Make this clear in your documentation.

Connect to VS Code

Create .vscode/settings.json:

{
  "[yaml]": {
    "editor.defaultFormatter": "redhat.vscode-yaml",
    "editor.formatOnSave": true
  },
  "[toml]": {
    "editor.defaultFormatter": "denoland.vscode-deno"
  }
}

And .vscode/launch.json to debug the server:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "go",
      "name": "Config Language Server",
      "request": "launch",
      "program": "${workspaceFolder}/cmd/lsp-server",
      "cwd": "${workspaceFolder}"
    }
  ]
}

Complete Example: Putting It All Together

Here's a minimal but complete example:

package main

import (
    "fmt"
    "log"
    "os"
    
    "github.com/realharshthakur/c4"
    "github.com/realharshthakur/c4/lsp"
)

type ServerConfig struct {
    Host string `yaml:"host"`
    Port int    `yaml:"port"`
}

func (c *ServerConfig) ConfigSchema(b *c4.SchemaBuilder) {
    b.Description("Server configuration")
    b.Field(&c.Host).Default("localhost").Description("Server hostname")
    b.Field(&c.Port).Default(8080).GreaterThan(0).LessThan(65535).Description("Server port")
}

func main() {
    if len(os.Args) > 1 && os.Args[1] == "lsp" {
        // Start LSP server mode
        if err := lsp.Run(map[string]any{"ServerConfig": &ServerConfig{}},
            lsp.Options{
                Name:               "Server Config LSP",
                Version:            "1.0.0",
                HoverEnabled:       true,
                CompletionEnabled:  true,
                DiagnosticsEnabled: true,
            }); err != nil {
            panic(err)
        }
        return
    }
    
    // Load and use config
    var cfg ServerConfig
    if err := c4.UnmarshalFile("server.yaml", &cfg); err != nil {
        log.Fatalf("Config error: %v", err)
    }
    
    fmt.Printf("Server: %s:%d\n", cfg.Host, cfg.Port)
    
    // Generate docs
    docs := c4.GenerateDocs(&ServerConfig{})
    os.WriteFile("SERVER_CONFIG.md", []byte(docs), 0644)
}

Usage:

# Generate docs
go run . > SERVER_CONFIG.md

# Start LSP server (editor connects to stdin/stdout)
go run . lsp

# Load and validate config
go run .

Next Steps


Contributing

We welcome contributions from the community! Whether it's bug reports, feature requests, documentation improvements, or code contributions, please feel free to open an issue or submit a pull request.

Please ensure your contributions:

  • Follow the existing code style and patterns
  • Include tests for new functionality
  • Update documentation as needed
  • Reference any related issues in your PR

License

C4 is licensed under the Apache License 2.0. See LICENSE for details.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages