Skip to content

Instantly share code, notes, and snippets.

@pelletier
Created October 23, 2021 19:45
Show Gist options
  • Save pelletier/490fc68cbba3e2f0f1f3fd2457188527 to your computer and use it in GitHub Desktop.
Save pelletier/490fc68cbba3e2f0f1f3fd2457188527 to your computer and use it in GitHub Desktop.
Proposal for document editing structure API for go-toml/v2.
// Package document provides tools for manipulating the structure of TOML
// documents.
//
// While github.com/pelletier/go-toml provides efficient functions to transform
// TOML documents to and from usual Go types, this package allows you to create
// and modify the structure of a TOML document.
//
// Comments
//
// Most structural elements of a Document can have comments attached to them.
// Those elements have a Comment field in their struct that can be manipulated
// directly. Comments can either be above the element they decorate (default) or
// inline. If the comment is inline, all newlines are removed from the comment's
// text when the Document is represented as TOML. In addition, most elements can
// be commented-out using their Commented field. Because the parser is not able
// to detect that a given element is present inside a comment, this field is
// only used during the encoding of the document.
//
// Design decisions
//
// Document does not represent white space. When parsing a document from bytes,
// all white space is discarded. The only control over white space is provided
// by the document encoder in the form of general rules. One use case that is
// not covered is modifying an existing TOML document, while keeping the
// non-modified part of the document exactly the same byte-for-byte. However it
// simplifies the API and parsing significantly.
//
// It is a design goal to be able to write literal Documents and modify them
// without too much assistance. For example, instead of providing dozens of
// Create / Modify / Delete functions for all kinds of nodes, the current design
// provides allows the user to manipulate pointers and slices like any other Go
// data structure. The drawback is that the operations performed on the Document
// cannot be validated immediately. A certain amount of constraint is added in
// the form of typing, but ultimately it is the responsibility of the user to
// call Valid() after reading or before writing a Document, if they wishes to
// only deal with valid documents.
//
// While many operations would feel natural on maps, this Document structure
// actually only contains slices of elements to represent parent / children
// relationships. This allows the user to completely control the ordering of
// their document, as well as its exact shape. For example, the following valid
// documents can all be represented:
//
// a.b.c = 42
//
// [a]
// b.c = 42
//
// [a.b]
// c = 42
//
// [a]
// b = { c = 42 }
//
// [a]
// [a.b]
// c = 42
//
// [a.b]
// c = 42
// [a]
//
// Comments are a first class object in this model. An often requested feature
// is to preserve and manipulate comments in TOML documents. By embedding them
// in the core of every node, full control is provided to the user on how they
// want to comment their document.
//
// See the Examples for examples of classic Document usages.
package document
import (
"strconv"
)
// Document represents a TOML document.
type Document struct {
KeyValues []*KeyValue
Tables []*Table
// Optional last comment of the document.
TrailerComment Comment
}
// GetAt traverses the document to return a pointer to the Value stored at the
// path represented by parts. Returns nil if no such document exists.
//
// Even though part/s is of type interface{}, each of them should be either a
// string or an int. If it is a string, it is interpreted as a table or
// key-value key part. If it is an integer, it is interpreted as an array index.
// -1 is used to denote the last element of the array, if it exists. Any other
// type panics.
//
// This function operates on the structure of the document. If the path is not
// explicitly defined in the document this function returns nil.
func (d Document) GetAt(part interface{}, parts ...interface{}) Value {
// TODO
return nil
}
// ParentOf returns the immediate parent of a given Value. Panics if the parent
// does not exist. A classic use-case is to first call GetAt to retrieve a
// specific element, then call ParentOf to get the parent and possibly reorder
// or delete the element.
func (d Document) ParentOf(v Value) Value {
// TODO
return nil
}
// Valid verifies that the document is fully compliant with the TOML
// specification. It returns nil if it is valid, or a list of errors otherwise.
// While this function tries to find all errors, it does not guarantee to find
// them all if at least one error is found.
func (d Document) Valid() []error {
// TODO
return nil
}
// Key of a Table or KeyValue. The key parts are dot-separated in their TOML
// representation.
type Key []KeyPart
// KeyPart is an individual element in a key. If the KeyPart has been
// constructed manually there is no guarantee that Value is can be represented
// with Kind. Use Valid() to check.
type KeyPart struct {
// The actual text of the key. Cannot contain a new line character.
Value string
// One of bare, literal, or quoted.
Kind KeyKind
}
// Valid returns true if the part's Value can be represented with Kind.
func (k KeyPart) Valid() bool {
// TODO
return false
}
// KeyKind is a type to represent the kind of a key part. Kinds are mutually
// exclusive.
type KeyKind int
const (
// BareKey kind does not have any decoration. It may only contain ASCII
// letters, ASCII digits, underscores, and dashes (A-Za-z0-9_-).
BareKey KeyKind = iota
// LiteralKey kind are decorated with single quotes ('). They can
// contain any character except for new lines an single quotes.
LiteralKey
// QuotedKey kind are decorated with double quotes ("). They can contain
// any character except for new lines.
QuotedKey
)
// StringKey is a convenience function to generate a Key from strings. It is
// mostly useful when expressing documents as literals.
// The kind precedence of each part is BareKey > LiteralKey > QuotedKey.
func StringKey(part1 string, parts ...string) Key {
// TODO
return Key{}
}
// StringKind is a set of flags to represent the kind of string. They can be
// combined with bitwise-or.
//
//
// Example of a multiline literal string:
//
// Kind: LiteralString | MultilineString
//
// // Note that the LiteralString flag always takes precedence over
// // BasicString.
// LiteralString | BasicString == LiteralString
type StringKind int
const (
BasicString StringKind = 1 << iota
LiteralString
MultilineString
)
type KeyValue struct {
Range Range
Comment Comment
Commented bool
Key Key
Value Value
}
// Value is an interface supported by all the terminal types of a TOML document.
// Its contents are private to avoid allowing non-supported types to make their
// way by mistake into a TOML Document.
type Value interface {
isValue()
}
type String struct {
Range Range
Value string
Kind StringKind
}
func (s *String) isValue() {}
type Integer struct {
Range Range
V string
}
func (i *Integer) isValue() {}
func (i *Integer) Set(v int64) {
i.V = strconv.FormatInt(v, 10)
}
func (i *Integer) FromString(v string) {
i.V = v
}
func (i Integer) Value() int64 {
v, err := strconv.ParseInt(i.V, 10, 64)
if err != nil {
panic("document should not let an invalid integer be stored")
}
return v
}
func (i Integer) String() string {
return i.V
}
type Boolean struct {
Range Range
V bool
}
func (b *Boolean) isValue() {}
// TODO: Float should be the same as Integer
// TODO: different types of dates should follow the same model.
type Array struct {
Comment Comment
Commented bool
Range Range
// Should each element of the array be on its own line. If false,
// Comments / Commented attributes of the elements are ignored.
Multiline bool
Elements []ArrayElement
}
func (a *Array) isValue() {}
type ArrayElement struct {
Comment Comment
Commented bool
Range Range
Value Value
}
// InlineTable represents an inline definition of a table. It can only be used
// inside a KeyValue value.
type InlineTable struct {
Range Range
Elements []*KeyValue
}
// Table is a structural element of a TOML document. It contains a key and zero
// or more key values.
type Table struct {
// Optional comment either above the table or on the same line as the
// table's Key.
//
// For example:
//
// # A comment above.
// [table] # A comment inline.
// ...
Comment Comment
Commented bool
// Range of the [header].
Range Range
// Whether the table is actually an array table (key in double square
// brackets).
Array bool
Key Key
Elements []*KeyValue
}
// Comment is usually a member of an element of the TOML document. Comments can
// be either above or inline with the element they decorate.
type Comment struct {
// Can be contain new line characters. An empty value means no comment.
Value string
Inline bool
Range Range
}
// Zero indicates whether a comment has any value.
func (c Comment) Zero() bool {
return c.Value == ""
}
// Position indicates a specific location in the original document.
type Position struct {
// Starts at 1.
Row int
// Starts at 1.
Column int
// Starts at 0.
Byte int
}
// Range represents the location of the annotated element in the original
// document. Its content is filled by the parser and never used.
type Range struct {
// Inclusive, in the document.
Start Position
// Exclusive, potentially past the last character of the document.
Stop Position
}
package document_test
import (
"fmt"
"github.com/pelletier/go-toml/v2/document"
)
func ExampleDocument_walk() {
// TODO (https://golang.org/src/go/ast/walk.go)
}
func ExampleDocument_getAt() {
doc := document.Document{
KeyValues: []*document.KeyValue{
{
Key: document.StringKey("array"),
Value: &document.Array{
Elements: []document.ArrayElement{
{Value: &document.String{Value: "zero"}},
{Value: &document.String{Value: "one"}},
{Value: &document.String{Value: "two"}},
},
},
},
},
Tables: []*document.Table{
{
Key: document.StringKey("a", "b"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("c"),
Value: &document.String{Value: "value"},
},
},
},
},
}
// Can retrieve explicit tables.
fmt.Println("table:", doc.GetAt("a", "b"))
// Can retrieve leaf nodes.
fmt.Println("leaf:", doc.GetAt("a", "b", "c"))
// Returns nil for nonexistent nodes.
fmt.Println("nonexistent:", doc.GetAt("doesnotexist"))
// Does not retrieve implicit tables.
fmt.Println("implicit:", doc.GetAt("a"))
// Can use index to get inside an array.
fmt.Println("index:", doc.GetAt("array", 1))
// Index outside of the range of an array returns nil.
fmt.Println("oob:", doc.GetAt("array", 42))
// Index can be -1 to mean the last element of the array.
fmt.Println("last:", doc.GetAt("array", -1))
// Output:
// table: {{"a", "b"}, {"c", "value}}
// leaf: {"value"}
// nonexistent: nil
// implicit: nil
// index: {"one"}
// oob: nil
// last: {"two"}
}
func ExampleDocument_arrayTable() {
doc := document.Document{
Tables: []*document.Table{
{
Array: true,
Key: document.StringKey("products"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("name"),
Value: &document.String{Value: "Hammer"},
},
{
Key: document.StringKey("sku"),
Value: &document.Integer{V: "738594937"},
},
},
},
{
Array: true,
Key: document.StringKey("products"),
Comment: document.Comment{
Value: "empty table within the array",
Inline: true,
},
},
{
Array: true,
Key: document.StringKey("products"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("name"),
Value: &document.String{Value: "Nail"},
},
{
Key: document.StringKey("sku"),
Value: &document.Integer{V: "284758393"},
},
{
Key: document.StringKey("color"),
Value: &document.String{Value: "gray"},
},
},
},
},
}
fmt.Printf("%+v", doc)
// Output:
// [[products]]
// name = "Hammer"
// sku = 738594937
//
// [[products]] # empty table within the array
//
// [[products]]
// name = "Nail"
// sku = 284758393
// color = "gray"
}
func ExampleDocument_reference() {
doc := document.Document{
KeyValues: []*document.KeyValue{
{
Key: document.StringKey("title"),
Value: &document.String{Value: "TOML Example"},
},
},
Tables: []*document.Table{
{
Key: document.StringKey("owner"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("name"),
Value: &document.String{Value: "Tom Preston-Werner"},
},
// TODO: dob
},
},
{
Key: document.StringKey("database"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("enabled"),
Value: &document.Boolean{V: true},
},
{
Key: document.StringKey("ports"),
Value: &document.Array{
Elements: []document.ArrayElement{
{Value: &document.Integer{V: "8000"}},
{Value: &document.Integer{V: "8001"}},
{Value: &document.Integer{V: "8002"}},
},
},
},
{
Key: document.StringKey("data"),
Value: &document.Array{
Elements: []document.ArrayElement{
{Value: &document.Array{
Elements: []document.ArrayElement{
{Value: &document.String{Value: "delta"}},
{Value: &document.String{Value: "phi"}},
},
}},
{Value: &document.Array{
Elements: []document.ArrayElement{
// TODO floats
// document.Float{V: "3.14"},
},
}},
},
},
},
},
},
{
Key: document.StringKey("servers"),
},
{
Key: document.StringKey("servers", "alpha"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("ip"),
Value: &document.String{Value: "127.0.0.1"},
},
{
Key: document.StringKey("role"),
Value: &document.String{Value: "frontend"},
},
},
},
{
Key: document.StringKey("servers", "beta"),
Elements: []*document.KeyValue{
{
Key: document.StringKey("ip"),
Value: &document.String{Value: "127.0.0.2"},
},
{
Key: document.StringKey("role"),
Value: &document.String{Value: "backend"},
},
},
},
},
}
fmt.Println(doc)
// Output:
// title = "TOML Example"
//
// [owner]
// name = "Tom Preston-Werner"
// dob = 1979-05-27T07:32:00-08:00
//
// [database]
// enabled = true
// ports = [ 8000, 8001, 8002 ]
// data = [ ["delta", "phi"], [3.14] ]
// temp_targets = { cpu = 79.5, case = 72.0 }
//
// [servers]
//
// [servers.alpha]
// ip = "10.0.0.1"
// role = "frontend"
//
// [servers.beta]
// ip = "10.0.0.2"
// role = "backend"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment