Documentation
¶
Overview ¶
Package godotyaml is the reference Go library for reading and writing go.yaml, a centralized config and metadata file for Go projects.
It is the parser/helper layer other tools depend on so they do not each roll their own YAML handling for go.yaml. A go.yaml file has two zones: a closed set of root project metadata (name, version, schema_version, executables, ...) exposed here as typed accessors, and an open external namespace where each tool stores arbitrary config under external.<toolname>. New builds a document from that root metadata, so a tool offering an `init` command does not have to assemble the file itself. External sections are treated as opaque: the library reads them, hands them back to the caller to decode, and writes a single tool's section back without disturbing the rest of the file.
Writing a section comes in two flavours, and which one a tool wants is a real decision rather than a detail. SetExternalConfig replaces external.<toolname> outright, so keys and hand-written comments inside it that the caller did not supply are discarded. MergeExternalConfig merges into it instead, leaving everything the caller did not mention (including comments) in place, at the cost of never removing anything. RemoveExternalConfig and RemoveExternalConfigKey are how things are removed deliberately.
The yaml.v3 node tree is kept as the internal source of truth so that comments, key ordering, and unknown root keys survive a load/save cycle, and so that updating one tool's section never re-serializes (and never corrupts) the rest of the document.
Non-goals ¶
godotyaml is deliberately small and unopinionated. It does NOT:
- validate the semantic correctness of any value (URLs, SPDX license identifiers, version strings, and so on are returned verbatim);
- enforce any schema on the external namespace;
- refuse to parse files with unknown root keys or unknown schema_version values — both are preserved and surfaced rather than rejected;
- expose helpers specific to any individual tool. No consuming tool is privileged; every tool's config lives at external.<toolname> on equal footing.
It is not a CLI, not a validator, and not a schema enforcer.
Index ¶
- type Author
- type Document
- func (d *Document) Authors() ([]Author, error)
- func (d *Document) DecodeExternalConfig(name string, out any) (bool, error)
- func (d *Document) Description() string
- func (d *Document) Documentation() string
- func (d *Document) Executables() (Executables, error)
- func (d *Document) ExternalConfigNames() []string
- func (d *Document) GetRawExternalConfig(name string) (*yaml.Node, bool)
- func (d *Document) Homepage() string
- func (d *Document) IssueTracker() string
- func (d *Document) License() string
- func (d *Document) MergeExternalConfig(name string, value any) error
- func (d *Document) Name() string
- func (d *Document) RemoveExternalConfig(name string) bool
- func (d *Document) RemoveExternalConfigKey(name, key string) bool
- func (d *Document) Repository() string
- func (d *Document) Save(path string) error
- func (d *Document) SaveNew(path string) error
- func (d *Document) SchemaVersion() (int, error)
- func (d *Document) SetExternalConfig(name string, value any) error
- func (d *Document) SetExternalConfigKey(name, key string, value any) error
- func (d *Document) Version() string
- func (d *Document) Write(w io.Writer) error
- type Executable
- type Executables
- type Metadata
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Author ¶
type Author struct {
Name string `yaml:"name"`
Email string `yaml:"email,omitempty"`
Organization string `yaml:"organization,omitempty"`
URL string `yaml:"url,omitempty"`
}
Author describes a single entry in the root authors field. Only Name is expected to be present and the remaining fields are optional and tools may carry additional information the spec adds later by decoding the raw node directly.
type Document ¶
type Document struct {
Doc *yaml.Node // the document node returned by the decoder
Root *yaml.Node // the root mapping node (doc.Content[0])
}
Document is a parsed go.yaml file.
The underlying yaml.v3 node tree is the source of truth: typed accessors read from it on demand, and mutations edit it in place. Keeping the node tree canonical (rather than unmarshaling into a struct) is what lets the library preserve comments, key ordering, and unknown keys across a load/save cycle.
func Load ¶
reads and parses the go.yaml file at given path
When no file exists at path the returned error satisfies os.IsNotExist (and errors.Is(err, fs.ErrNotExist)), so a caller can tell "this project has no go.yaml" apart from "this go.yaml is broken" without stat-ing the path first.
func New ¶ added in v0.2.0
builds a new go.yaml document from root metadata, for tools that offer an `init` command.
Keys are written in the order the spec lists them. omitDefaults decides what becomes of a field the caller left at its zero value:
- false produces a scaffold meant to be edited. Every scalar key is written, with a placeholder value for the ones the caller did not set, and authors, executables and external follow as commented-out examples. The point of the examples is that the shape of those three is the part nobody remembers, so having it in the file removes a trip to the spec.
- true leaves unset keys out of the file altogether, so a caller that knows only the project's name gets a three-line file rather than a scaffold. No examples are written either.
schema_version is written either way. Zero is the current schema version as well as the zero value, and a file that states which schema it was written against is far easier to migrate later.
The examples are commented out rather than written as empty collections so that they are inert: the generated file parses to exactly the metadata the caller passed, and a tool reading it back sees no authors and no executables rather than examples it might mistake for real entries.
Nothing here is validated, in keeping with the rest of the library: an empty name, or a version that is not a version, is written out as given.
No external section is created; call SetExternalConfig or MergeExternalConfig on the result to add one, then Save (or SaveNew) to write the file. A section added that way is written above the commented examples, which stay in the file as documentation for the next tool.
func Parse ¶
reads a go.yaml document from the given io.Reader
An empty input yields an empty document with a writable root mapping rather than an error, so callers can build a file from scratch.
func (*Document) Authors ¶
Authors returns the root authors field as a list.
The field may appear in the file as a bare string, a single mapping, or a sequence of strings and/or mappings; all forms are normalized to []Author. A bare string becomes an Author with only Name set. It returns nil when the field is absent and an error only if a mapping entry cannot be decoded.
func (*Document) DecodeExternalConfig ¶
decodes external.<name> into out, reporting whether the section exists. It is a thin convenience over GetRawExternalConfig followed by node.Decode.
func (*Document) Description ¶
Description returns the root description field, or "" if absent.
func (*Document) Documentation ¶
Documentation returns the root documentation field (the project's docs URL), or "" if absent.
func (*Document) Executables ¶
func (d *Document) Executables() (Executables, error)
Executables returns the root executables field, or nil if absent. Absence and an empty map are equivalent. It returns an error only if the section does not match the Executables shape.
func (*Document) ExternalConfigNames ¶
returns the config names present under external, in document order.
func (*Document) GetRawExternalConfig ¶
returns the raw yaml.Node for external.<name>, reporting whether the section exists.
The node is returned so the caller can decode it into its own types (node.Decode(&out)) without godotyaml imposing a structure on it. The node is the live tree node; treat it as read-only and use SetExternalConfig to write changes.
func (*Document) Homepage ¶
Homepage returns the root homepage field (the project's website), or "" if absent.
func (*Document) IssueTracker ¶
IssueTracker returns the root issue_tracker field, or "" if absent.
func (*Document) MergeExternalConfig ¶ added in v0.2.0
merges value into external.<name>, creating the section if needed, keeping the keys and comments the caller did not supply.
Mappings are merged recursively at every depth: a key present in value takes the value given, and a key absent from value is left exactly as it was, together with any comment attached to it. Sequences and scalars are not merged but replaced wholesale, which is what lets a caller shorten or clear a list by supplying the new one.
A merge never removes anything. A tool that drops a key from its own config struct and then merges will still find the old key in the file, because "absent from value" means "leave it alone", not "delete it". To remove something use RemoveExternalConfigKey for a single key, SetExternalConfigKey to replace one top-level key of the section wholesale, or SetExternalConfig to replace the whole section.
Comments already in the file survive the merge. A comment carried on value (for callers that build their own *yaml.Node) replaces the comment on the matching key, but an empty comment never erases one that is already in the file.
Only external.<name> is touched: sibling sections and the remainder of the file remain intact and untouched.
func (*Document) RemoveExternalConfig ¶ added in v0.2.0
removes external.<name>, reporting whether the section was present.
The external mapping itself is never removed, even when the section removed was the last one. An empty external renders as "external: {}", which keeps any comment written above external; removing the mapping as well would silently discard that comment, which is the kind of loss this library exists to avoid. A later write repopulates the empty mapping in block style as usual.
func (*Document) RemoveExternalConfigKey ¶ added in v0.2.0
removes external.<name>.<key>, reporting whether the key was present.
The comments attached to that key go with it. Neighbouring keys, the rest of the section, and the rest of the file are untouched. Removing the last key of a section leaves an empty section behind; use RemoveExternalConfig to remove the section itself.
func (*Document) Repository ¶
Repository returns the root repository field, or "" if absent.
func (*Document) Save ¶
writes the document back to the file at path.
The write is atomic. The document is rendered fully in memory, written to a temporary file beside path, flushed, and then renamed over path, so a serialization error, a crash, or a full disk cannot leave a truncated go.yaml behind: path always holds either the previous content or the complete new content. A go.yaml usually carries several tools' configuration as well as the project metadata, so a half-written one is expensive to lose.
An existing file keeps its permission bits exactly; because a rename takes the mode of the temporary file, Save copies the mode across itself. A file that Save creates is created 0644 as modified by the process umask. When path is a symlink the link is resolved and its target replaced, leaving the link itself in place.
func (*Document) SaveNew ¶ added in v0.2.0
writes the document to path only if no file is there yet.
This is the write an `init` command wants. A go.yaml holds every tool's configuration as well as the project metadata, so overwriting one that already exists destroys other tools' data; SaveNew refuses instead of clobbering. If path exists the returned error satisfies os.IsExist (and errors.Is(err, fs.ErrExist)), which the caller can report as "this project already has a go.yaml".
The check is not a stat followed by a write: the file is created exclusively, so two `init` runs racing each other cannot both decide the path was free. The content is then written with the same atomic replace Save uses.
func (*Document) SchemaVersion ¶
SchemaVersion returns the root schema_version as an integer.
The schema version is a single incrementing integer (0, 1, 2, ...). There are no minor versions such as 1.1. It returns (0, nil) when the key is absent. If the value is present but not a valid integer it returns a non-nil error: the file still parses (Load/Parse never reject it) and the malformed value is surfaced here rather than silently coerced. Quoted scalars (e.g. "1") are accepted.
func (*Document) SetExternalConfig ¶
writes external.<name>, creating the external section if needed.
The section is REPLACED, not merged: every key currently under external.<name> is discarded, including keys the caller did not supply and any comment written by hand inside the section. A comment attached to the external.<name> key itself survives, because that key node is reused. Use MergeExternalConfig when the section's other keys and comments have to be kept.
value may be any value yaml.v3 can marshal, or an existing *yaml.Node for callers that manage their own subtree (e.g. to preserve their own comments).
This function only touches the target section of `external` and sibling objects and the remainder of the file remains intact and untouched
func (*Document) SetExternalConfigKey ¶ added in v0.2.0
writes external.<name>.<key>, creating the section (and external) if needed.
The value at key is replaced wholesale, so any mapping previously stored there is discarded along with the comments attached to that value. A comment attached to the key itself is kept, as is every other key of the section. Use this to replace one top-level key of a section outright where MergeExternalConfig would merge into it.
If external.<name> exists but does not hold a mapping, it is replaced by one.
func (*Document) Write ¶
serializes the document to the given io.Writer, preserving comments and key ordering
Indentation is normalized to two spaces; yaml.v3 does not record the source file's original indent width, so that one stylistic detail is not preserved. Structure and ordering are otherwise reproduced as they were read: key order, comments, unknown root keys, and every external section the caller did not touch are written back unchanged.
Blank lines are NOT preserved. yaml.v3 has no representation for a blank line, so the empty lines a human used to separate keys and sections are all dropped the first time a tool writes the file. This is usually the most visible part of the diff a tool produces, and it is a limitation of the underlying YAML library rather than a choice godotyaml makes.
type Executable ¶
type Executable struct {
Entrypoint string `yaml:"entrypoint"` // directory holding package main, relative to project root
Description string `yaml:"description,omitempty"` // optional human-readable purpose
}
Executable describes one executable entry point the project produces. It is project metadata only as it carries no output paths, OS/arch targets, build flags, or per-executable versions (every executable inherits the single project version). Build-specific concerns belong in a build tool's external.<toolname> section, not here.
type Executables ¶
type Executables map[string]Executable
Executables maps an author-chosen executable name to its definition. Absence of the root key and an empty map are equivalent (both have length 0): the project produces no executables, i.e. it is a library.
type Metadata ¶ added in v0.2.0
type Metadata struct {
Name string
Description string
Version string
SchemaVersion int
Repository string
IssueTracker string
Homepage string
Documentation string
License string
Authors []Author
Executables Executables
}
Metadata is the root project metadata of a go.yaml, as a plain struct, so a tool can hand the whole of it to New at once.
Every field is optional, matching the spec. A field left at its zero value is either written with a default value or left out of the file entirely, depending on New's omitDefaults argument.