Created
October 23, 2021 19:45
-
-
Save pelletier/490fc68cbba3e2f0f1f3fd2457188527 to your computer and use it in GitHub Desktop.
Proposal for document editing structure API for go-toml/v2.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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