{"id":34641673,"url":"https://github.com/rupor-github/gencfg","last_synced_at":"2026-04-18T05:04:02.162Z","repository":{"id":264231087,"uuid":"892769711","full_name":"rupor-github/gencfg","owner":"rupor-github","description":"Helpers to handle Yaml configuration in go code.","archived":false,"fork":false,"pushed_at":"2025-11-01T21:36:09.000Z","size":100,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-01T23:26:15.856Z","etag":null,"topics":["configuration","go","golang","yaml","yaml-configuration"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rupor-github.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-22T18:38:21.000Z","updated_at":"2025-11-01T21:35:59.000Z","dependencies_parsed_at":"2024-12-16T19:28:04.646Z","dependency_job_id":"35e3ac65-cfdf-43f4-bb6c-b4df5337ba27","html_url":"https://github.com/rupor-github/gencfg","commit_stats":null,"previous_names":["rupor-github/gencfg"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/rupor-github/gencfg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupor-github%2Fgencfg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupor-github%2Fgencfg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupor-github%2Fgencfg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupor-github%2Fgencfg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rupor-github","download_url":"https://codeload.github.com/rupor-github/gencfg/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rupor-github%2Fgencfg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31957158,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["configuration","go","golang","yaml","yaml-configuration"],"created_at":"2025-12-24T17:19:12.274Z","updated_at":"2026-04-18T05:04:02.151Z","avatar_url":"https://github.com/rupor-github.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\ngencfg\n========\n\nCombination of importable module and a CLI tool on top of it to simplify dealing\nwith YAML configuration files in Go code.\n\nRather than using 3rd party packages to handle configuration lately it seems\npreferable to use Go built in support for marshaling and struct tags. Taking\ninto account requirements of local development and testing it's difficult to\nprevent configuration logic from spreading across your code. Package\nprovides some tooling to solve this problem to a degree allowing most of the\nlogic (including setting defaults) to be in the configuration template\n(possibly embedded into resulting binary). \n\nIt introduces an ability to use [Go Templating engine](https://golang.org/pkg/text/template/)\nwith added support for [slim-sprig lib](https://go-task.github.io/slim-sprig/)\nand some small extensions to derive \"values\" in the configuration. This\nmakes it possible to both generate configuration files and to create\nconfigurations for production usage, development and testing in a coherent way\nhopefully from a single configuration template.\n\n## Template variables defined by project\n\n    .Name (string) - name of the YAML node value is being assigned to\n    .ProjectDir (string) - used to expand relative paths in configuration, could be passed in\n    .Hostname (string) - Go's os.Hostname()\n    .IPv4 (string) - IPv4 address of local host, not loopback address\n    .Containerized (bool) - true if code is executed in container (checks for /.dockerenv or /.containerenv)\n    .Testing (bool) - true if expansion happens when code is being run with \"go test\"\n    .CPUs (int) - Go's runtime.NumCPU()\n    .OS (string) - Go's runtime.GOOS\n    .ARCH (string) - Go's runtime.GOARCH\n    .Arguments (map[string]string) - could be passed to Process() using WithArgument(name, value) calls\n\n## Template functions defined by project in addition to sprig\n\n    joinPath - Joins any number of arguments into a path. The same as Go's filepath.Join.\n    freeLocalPort - takes no arguments, returns free unique local port to be used for testing. For running tests in parallel implementation keeps global port map.\n\n## How template expansion works\n\nProcess() parses the input YAML into a node tree and walks it depth-first.\nOnly string values (`!!str` YAML tag) inside mapping nodes (key-value pairs)\nare candidates for expansion. Sequence items, mapping keys, and non-string\nvalues are never expanded.\n\nA value is expanded when it matches `{{.*}}` and its field name is not in the\n\"do not expand\" set. The expanded result is re-parsed as YAML, which enables\nautomatic type coercion - for example a template that produces `true` will\nbecome a YAML boolean, and `42` will become an integer.\n\nChildren are expanded before parents (depth-first) to prevent infinite loops\nfrom self-referential templates.\n\nAll environment-dependent values (hostname, IPv4, container detection, etc.)\nare computed once per Process() call, not per-field.\n\n## Example of using in your code, just to give you an idea\n\n    import (\n        _ \"embed\"\n        \"github.com/rupor-github/gencfg\"\n    )\n\n    // Embedded configuration template - provides smart defaults\n    //go:embed config.yaml.tmpl\n    var ConfigTmpl []byte\n\n    // Configuration structures\n    type (\n        Config struct {\n            SourcePath   string `yaml:\"source\" sanitize:\"path_abs,path_toslash\" validate:\"required,dir\"`\n            TargetPath   string `yaml:\"target\" sanitize:\"path_clean,path_toslash\" validate:\"required,filepath|email\"`\n        }\n    )\n\n    func unmarshalConfig(data []byte, cfg *Config, process bool) (*Config, error) {\n        // We want to use only fields we defined so we cannot use yaml.Unmarshal directly here\n        dec := yaml.NewDecoder(bytes.NewReader(data))\n        dec.KnownFields(true)\n        if err := dec.Decode(cfg); err != nil {\n            return nil, fmt.Errorf(\"failed to decode configuration data: %w\", err)\n        }\n        if process {\n            // sanitize and validate what has been loaded\n            if err := gencfg.Sanitize(cfg); err != nil {\n                return nil, err\n            }\n            if err := gencfg.Validate(cfg, gencfg.WithAdditionalChecks(checks)); err != nil {\n                return nil, err\n            }\n        }\n        return cfg, nil\n    }\n\n    // LoadConfiguration reads the configuration from the file at the given path, superimposes its values on\n    // top of expanded configuration template to provide sane defaults and performs validation.\n    func LoadConfiguration(path string, options ...func(*gencfg.ProcessingOptions)) (*Config, error) {\n        haveFile := len(path) \u003e 0\n\n        data, err := gencfg.Process(ConfigTmpl, options...)\n        if err != nil {\n            return nil, fmt.Errorf(\"failed to process configuration template: %w\", err)\n        }\n        cfg, err := unmarshalConfig(data, \u0026Config{}, !haveFile)\n        if err != nil {\n            return nil, fmt.Errorf(\"failed to process configuration template: %w\", err)\n        }\n        if !haveFile {\n            return cfg, nil\n        }\n\n        // overwrite cfg values with values from the file\n        data, err = os.ReadFile(path)\n        if err != nil {\n            return nil, fmt.Errorf(\"failed to read config file: %w\", err)\n        }\n        cfg, err = unmarshalConfig(data, cfg, haveFile)\n        if err != nil {\n            return nil, fmt.Errorf(\"failed to process configuration file: %w\", err)\n        }\n        return cfg, nil\n    }\n\nProcessingOptions allows specifying root directory for expanding relative paths\nin configuration uniformly (WithRootDir), passing additional arguments to\ntemplates (WithArgument) and marking some fields not to be expanded as\ntemplates (WithDoNotExpandField). You could also add some custom validation\ncode if necessary (see below).\n\n## Command line tool\n\nSometimes you may want to get actual configuration file for your project. Use\nCLI tool from this project (could be part of your build). It's also good\nexample of how to use imported gencfg in your code.\n\n    ❯ gencfg -h\n\n    NAME:\n       gencfg - generate configuration file from template\n\n    USAGE:\n       gencfg [options] TEMPLATE [DESTINATION]\n\n    OPTIONS:\n       --project-dir value, -d value  Project directory to use for expansion (default is current directory)\n       --literal value, -l value      Name of the field(s) not to be treated as template\n       --argument value, -a value     Additional argument(s) for template expansion in key=value format\n       --help, -h                     show help (default: false)\n       --version, -v                  print the version (default: false)\n\nIf DESTINATION is not specified, the output is written to stdout.\n\nThe `--literal` and `--argument` flags can be specified multiple times:\n\n    gencfg -d /opt/myproject \\\n           -a env=production -a region=us-east-1 \\\n           -l raw_template_field \\\n           config.yaml.tmpl config.yaml\n\n## Some examples of template expansion in configuration\n\nReading database user/password from environment and using defaults otherwise:\n\n    db:\n        username: '{{ default \"user\" (env \"DB_USERNAME\") }}'\n        password: '{{ default \"pass\" (env \"DB_PASSWORD\") }}'\n\nSetting parameters for logging from environment:\n\n    logging:\n            level: info\n            # do not use \"log timestamps\" when running inside docker, rely on journald and docker logs to maintain timestamps\n            use_timestamp: \"{{ not .Containerized }}\"\n\nUsing arguments passed to Process():\n\n    environment: '{{ index .Arguments \"env\" }}'\n\nUsing the field name for self-referencing defaults:\n\n    sources: \"{{ joinPath .ProjectDir .Name }}\"\n\nThis would set `sources` to the joined path of ProjectDir and \"sources\".\n\n## Sanitizing configuration values\n\n`gencfg` module has additional capability of sanitizing configuration values.\nYou can set \"sanitize\" tag on a configuration field and call Sanitize() function\nafter your configuration unmarshaled into the struct in your code.\n\n    type SomeConfig struct {\n        WorkerPoolSize uint32 `yaml:\"worker_pool_size\"`\n        TempDir        string `yaml:\"temp_dir\" sanitize:\"path_clean\"`\n    }\n\n    cfg := \u0026SomeConfig{}\n    // Code to initialize cfg structure goes here (read from file, unmarshal, set...)\n    .........\n\n    // sanitize will call filepath.Clean() on TempDir value and assign the resulting cleaned path back to the TempDir field.\n\tif err := gencfg.Sanitize(\u0026cfg); err != nil {\n\t\t// Processing sanitization errors here\n        .........\n\t}\n\nRecognized actions called on fields with tags so path will be reset if\nnecessary. Sanitize supports comma-separated list of actions and calls them in\ndefinition order.\n\nSanitize recursively walks the struct, including nested structs, pointers,\nslices, arrays, and maps of structs. Unexported fields are skipped. Tags on\na parent struct field are NOT propagated to child struct fields - each struct\nmust define its own sanitize tags.\n\nPresently defined actions:\n\n    path_clean - same as calling filepath.Clean(value) on the configuration field\n    path_toslash - same as calling filepath.ToSlash(value) on the configuration field\n    path_abs - same as calling filepath.Abs(value) on the configuration field\n    assure_dir_exists - will call os.MkdirAll(value), not changing field itself\n    assure_dir_exists_for_file - will call os.MkdirAll(filepath.Dir(value)), not changing field itself\n    assure_file_access - will do filepath.Abs and os.Stat on the result\n    oneof_or_tag - checks if value is in whitespace-separated list of allowed values; if not, applies the last token as a sanitize tag\n                   Example: sanitize:\"oneof_or_tag=opt1 opt2 opt3 path_clean\" - if value is opt1, opt2, or opt3, leave as-is; otherwise apply path_clean\n\nAll string-based actions are no-ops when the field value is empty.\n\n## Validating configuration values\n\n`gencfg` module has additional capability of validating configuration values\nusing code from [validator](https://pkg.go.dev/github.com/go-playground/validator/v10) project.\n\nNote, that when supported tags aren't sufficient you could pass in custom\nfunction to perform additional checks.\n\n    type SomeConfig struct {\n        WorkerPoolSize uint32 `yaml:\"worker_pool_size\" validate:\"required\"`\n        TempDir        string `yaml:\"temp_dir\" sanitize:\"path_clean,assure_dir_exists\" validate:\"required,gt=1\"`\n    }\n\n    // To perform \"unusual\" specific checking, most likely involves cross-field, cross embedded structures validation\n    func additionalChecks(sl validator.StructLevel) {\n\n        c := sl.Current().Interface().(SomeConfig)\n\n        if c.WorkerPoolSize == 999 {\n            sl.ReportError(c.WorkerPoolSize, \"WorkerPoolSize\", \"\", \"do not like size must be 666\", \"\")\n        }\n        ..........\n    }\n    ..........\n\n    cfg := \u0026SomeConfig{}\n    // Code to initialize cfg structure goes here (read from file, unmarshal, set...)\n    .........\n\n\tif err := gencfg.Sanitize(\u0026cfg); err != nil {\n\t\t// Processing sanitization errors here\n        .........\n\t}\n\tif err := gencfg.Validate(\u0026cfg, gencfg.WithAdditionalChecks(additionalChecks)); err != nil {\n\t\t// Processing validation errors here\n        .........\n\t}\n\nValidate() returns all found problems at once, it would not fail on each\nconsecutive violation encountered. Please, read\n[documentation](https://pkg.go.dev/github.com/go-playground/validator/v10#readme-baked-in-validations)\nfor more details on available checks.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frupor-github%2Fgencfg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frupor-github%2Fgencfg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frupor-github%2Fgencfg/lists"}