Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Document Builder type #13

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions document.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package gemini

import (
"bytes"
"fmt"
"io"
"strings"

"github.com/pkg/errors"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See commit message for dependency discussion

)

type DocumentSections []func(dw *DocumentWriter) error

func (s DocumentSections) Write(dw *DocumentWriter) (err error) {
for i := 0; i < len(s); i++ {
if err = s[i](dw); err != nil {
return err
}
}
return nil
}

// NewDocumentWriter creates a DocumentWriter.
func NewDocumentWriter(w io.Writer) *DocumentWriter {
return &DocumentWriter{
w: w,
}
}

type DocumentWriter struct {
w io.Writer
}

func (dw *DocumentWriter) NewLine() (err error) {
_, err = io.WriteString(dw.w, "\n")
return fmt.Errorf("error writing newline: %w", err)
}

func (dw *DocumentWriter) Header1(text string) (err error) {
if _, err = io.WriteString(dw.w, "# "); err != nil {
return fmt.Errorf("error writing header1: %w", err)
}
if _, err = io.WriteString(dw.w, text); err != nil {
return fmt.Errorf("error writing header1: %w", err)
}
if _, err = io.WriteString(dw.w, "\n"); err != nil {
return fmt.Errorf("error writing header1: %w", err)
}
return nil
}

func (dw *DocumentWriter) Header2(text string) (err error) {
if _, err = io.WriteString(dw.w, "## "); err != nil {
return fmt.Errorf("error writing header2: %w", err)
}
if _, err = io.WriteString(dw.w, text); err != nil {
return fmt.Errorf("error writing header2: %w", err)
}
if _, err = io.WriteString(dw.w, "\n"); err != nil {
return fmt.Errorf("error writing header2: %w", err)
}
return nil
}

func (dw *DocumentWriter) Quote(text string) (err error) {
if _, err = io.WriteString(dw.w, "> "); err != nil {
return fmt.Errorf("error writing quote: %w", err)
}
if _, err = io.WriteString(dw.w, text); err != nil {
return fmt.Errorf("error writing quote: %w", err)
}
if _, err = io.WriteString(dw.w, "\n"); err != nil {
return fmt.Errorf("error writing quote: %w", err)
}
return nil
}

func (dw *DocumentWriter) Line(text string) (err error) {
if _, err = io.WriteString(dw.w, text); err != nil {
return err
}
if _, err = io.WriteString(dw.w, "\n"); err != nil {
return fmt.Errorf("error writing line: %w", err)
}
return nil
}

// DocumentBuilder allows programmatic document creation using the builder pattern.
// DocumentBuilder supports the use of headers and footers, which are combined with the body at build time.
type DocumentBuilder struct {
header string
body *strings.Builder
footer string
}

// NewDocumentBuilder creates a DocumentBuilder.
func NewDocumentBuilder() *DocumentBuilder {
builder := new(strings.Builder)
return &DocumentBuilder{"", builder, ""}
}

// SetHeader sets a document header. The header is written before the document body during `Build()`.
func (doc *DocumentBuilder) SetHeader(header string) {
doc.header = header
}

// SetFooter sets a document footer. The footer is written after the document body during `Build()`.
func (doc *DocumentBuilder) SetFooter(footer string) {
doc.footer = footer
}

// AddLine appends a new line to the document. Adds a newline to the end of the line if none is present.
func (doc *DocumentBuilder) AddLine(line string) error {
_, err := doc.body.WriteString(line)
if err != nil {
return errors.Wrap(err, "Error writing to document")
}

if !strings.HasSuffix(line, "\n") {
_, err = doc.body.WriteString("\n")
if err != nil {
return errors.Wrap(err, "Error writing to document")
}
}

return nil
}

// AddH1Header appends an H1 (#) header line to the document.
func (doc *DocumentBuilder) AddH1Header(header string) error {
_, err := doc.body.WriteString("# ")
if err != nil {
return errors.Wrap(err, "Error writing to document")
}
return doc.AddLine(header)
}

// AddH2Header appends an H2 (##) header line to the document.
func (doc *DocumentBuilder) AddH2Header(header string) error {
_, err := doc.body.WriteString("## ")
if err != nil {
return errors.Wrap(err, "Error writing to document")
}
return doc.AddLine(header)
}

// AddH3Header appends an H3 (###) header line to the document.
func (doc *DocumentBuilder) AddH3Header(header string) error {
_, err := doc.body.WriteString("### ")
if err != nil {
return errors.Wrap(err, "Error writing header line to document")
}
return doc.AddLine(header)
}

// AddQuote appends a quote line to the document.
func (doc *DocumentBuilder) AddQuote(header string) error {
_, err := doc.body.WriteString("> ")
if err != nil {
return errors.Wrap(err, "Error writing quote to document")
}
return doc.AddLine(header)
}

// AddBullet appends an unordered list item to the document.
func (doc *DocumentBuilder) AddBullet(header string) error {
_, err := doc.body.WriteString("* ")
if err != nil {
return errors.Wrap(err, "Error writing bullet to document")
}
return doc.AddLine(header)
}

// ToggleFormatting appends a toggle formatting line to the document.
func (doc *DocumentBuilder) ToggleFormatting() error {
return doc.AddLine("```")
}

// AddLink appends an aliased link line to the document.
func (doc *DocumentBuilder) AddLink(url string, title string) error {
_, err := doc.body.WriteString("=> ")
if err != nil {
return errors.Wrap(err, "Error writing link to document")
}
_, err = doc.body.WriteString(url)
if err != nil {
return errors.Wrap(err, "Error writing link to document")
}
_, err = doc.body.WriteString("\t")
if err != nil {
return errors.Wrap(err, "Error writing link to document")
}
// AddLine to ensure there is a newline
return doc.AddLine(title)
}

// AddRawLink appends a link line to the document.
func (doc *DocumentBuilder) AddRawLink(url string) error {
_, err := doc.body.WriteString("=> ")
if err != nil {
return errors.Wrap(err, "Error writing raw link to document")
}
err = doc.AddLine(url)
return err
}

// Build builds the document into a serialized byte slice.
func (doc *DocumentBuilder) Build() ([]byte, error) {
buf := bytes.Buffer{}

// Write header
_, err := buf.WriteString(doc.header)
if err != nil {
return nil, errors.Wrap(err, "Error building document header")
}

// Write body
_, err = buf.WriteString(doc.body.String())
if err != nil {
return nil, errors.Wrap(err, "Error building document body")
}

// Write footer
_, err = buf.WriteString(doc.footer)
if err != nil {
return nil, errors.Wrap(err, "Error building document footer")
}

return buf.Bytes(), nil
}
137 changes: 137 additions & 0 deletions document_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package gemini

import (
"bytes"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestDocumentBuilder(t *testing.T) {
tests := []struct {
name string
f func() *DocumentBuilder
expected string
expectedErr error
}{
{
name: "an empty builder produces no output",
f: func() *DocumentBuilder {
return NewDocumentBuilder()
},
expected: "",
expectedErr: nil,
},
}
for _, tt := range tests {
tt := tt
result, err := tt.f().Build()
if err != tt.expectedErr {
t.Errorf("expected err %q, got %q", tt.expectedErr, err)
continue
}
actual := string(result)
if diff := cmp.Diff(tt.expected, actual); diff != "" {
t.Error(diff)
}
}
}

func BenchmarkDocumentBuilder(b *testing.B) {
// Version 1 = 223 ns per operation, with 5 allocations.
b.ReportAllocs()
for i := 0; i < b.N; i++ {
db := NewDocumentBuilder()
var err error
if err = db.AddH1Header("heading 1"); err != nil {
b.Error(err)
}
if err = db.AddH2Header("heading 2"); err != nil {
b.Error(err)
}
if err = db.AddLine("normal text"); err != nil {
b.Error(err)
}
if err = db.AddQuote("quote"); err != nil {
b.Error(err)
}
result, err := db.Build()
if err != nil {
b.Error(err)
}
if len(result) == 0 {
b.Error("expected output, but didn't get any")
}
}
}

func BenchmarkDocumentWriter(b *testing.B) {
b.ReportAllocs()
// By moving the creation of the io.Writer out of the type, the
// buffer can be reused, resulting in lower execution speeds.
// This comes in at 167.0 ns per operation, with zero allocations.
// In practical terms, the io.Writer is most likely a file, or the
// Gemini output stream.
w := new(bytes.Buffer)
for i := 0; i < b.N; i++ {
db := NewDocumentWriter(w)
var err error
if err = db.Header1("heading 1"); err != nil {
b.Error(err)
}
if err = db.Header2("heading 2"); err != nil {
b.Error(err)
}
if err = db.Line("normal text"); err != nil {
b.Error(err)
}
if err = db.Quote("quote"); err != nil {
b.Error(err)
}
if len(w.Bytes()) == 0 {
b.Error("expected output, but didn't get any")
}
w.Reset()
}
}

func TestDocumentSections(t *testing.T) {
tests := []struct {
name string
sections []func(*DocumentWriter) error
expected string
}{
{
name: "headers and footers are appended",
sections: []func(*DocumentWriter) error{
func(dw *DocumentWriter) error {
return dw.Header1("header")
},
func(dw *DocumentWriter) error {
return dw.Line("middle")
},
func(dw *DocumentWriter) error {
return dw.Header1("footer")
},
},
expected: `# header
middle
# footer
`,
},
}
for _, tt := range tests {
tt := tt
ds := DocumentSections(tt.sections)
w := new(bytes.Buffer)
dw := NewDocumentWriter(w)
err := ds.Write(dw)
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
if diff := cmp.Diff(tt.expected, w.String()); diff != "" {
t.Error(diff)
}
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
module github.com/a-h/gemini

go 1.14

require (
github.com/google/go-cmp v0.5.6 // indirect
github.com/pkg/errors v0.9.1
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=