~emersion/public-inbox

go-scfg: scfg: add Encoder and Write v1 SUPERSEDED

Sebastien Binet: 1
 scfg: add Encoder and Write

 3 files changed, 211 insertions(+), 0 deletions(-)
hi Simon,
Next
I was mainly mirroring the encoding/json API.

It was also to (later on) be able to type-switch on types that implement
the SCFG (un)marshaling interface (or just use reflect).
but I didn't want to write that before the patch you mention was merged
into main :)

ie:
```go
type T1 struct {
	Listen []string `scfg:"listen"`
}

type T2 struct {
	Listen []string
}

func (t2 T2) MarshalSCFG() ([]byte, error) { ... }

var (
	t1 T1
	t2 T2
	blk scfg.Block
	enc = scfg.NewEncoder(os.Stdout)
)

err = enc.Encode(t1)  // use reflect (TODO)
err = enc.Encode(t2)  // use MarshalSCFG (TODO)
err = enc.Encode(blk) // ok
```

enc.Encode(blk) mirrors what can be done with:

```go
var (
	msg = json.RawMessage(`{"hello":"world"}`)
	err = json.NewEncoder(os.Stdout).Encode(msg)
)
```

[...]
Next
sure.
Next
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/44748/mbox | git am -3
Learn more about email & git

[PATCH go-scfg] scfg: add Encoder and Write Export this patch

Signed-off-by: Sebastien Binet <s@sbinet.org>
---
 marshal.go      | 107 ++++++++++++++++++++++++++++++++++++++++++++++++
 marshal_test.go |  94 ++++++++++++++++++++++++++++++++++++++++++
 writer.go       |  10 +++++
 3 files changed, 211 insertions(+)
 create mode 100644 marshal.go
 create mode 100644 marshal_test.go
 create mode 100644 writer.go

diff --git a/marshal.go b/marshal.go
new file mode 100644
index 0000000..5b661e9
--- /dev/null
+++ b/marshal.go
@@ -0,0 +1,107 @@
package scfg

import (
	"bytes"
	"fmt"
	"io"
)

// Marshal returns the SCFG encoding of v.
func Marshal(v interface{}) ([]byte, error) {
	buf := new(bytes.Buffer)
	enc := NewEncoder(buf)
	err := enc.Encode(v)
	if err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// Encoder write SCFG directives to an output stream.
type Encoder struct {
	w   io.Writer
	ind []byte
	lvl int
	err error
}

// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
	return &Encoder{
		w:   w,
		ind: []byte("  "),
	}
}

func (enc *Encoder) push() {
	enc.lvl++
}

func (enc *Encoder) pop() {
	enc.lvl--
}

func (enc *Encoder) whdr() {
	for i := 0; i < enc.lvl; i++ {
		enc.write(enc.ind)
	}
}

func (enc *Encoder) write(p []byte) {
	if enc.err != nil {
		return
	}
	_, enc.err = enc.w.Write(p)
}

// Encode writes the SCFG encoding of v to the stream.
func (enc *Encoder) Encode(v interface{}) error {
	var err error

	switch v := v.(type) {
	case Block:
		err = enc.encodeBlock(v)
	case Directive:
		err = enc.encodeDir(v)
	default:
		panic(fmt.Errorf("scfg: encode not implemented for %T", v))
	}

	return err
}

func (enc *Encoder) encodeBlock(blk Block) error {
	for _, dir := range blk {
		enc.encodeDir(*dir)
	}
	return enc.err
}

func (enc *Encoder) encodeDir(dir Directive) error {
	enc.whdr()
	enc.write([]byte(dir.Name))
	if dir.Name != "" && len(dir.Params) > 0 {
		enc.write([]byte(" "))
	}
	for i, p := range dir.Params {
		if i > 0 {
			enc.write([]byte(" "))
		}
		enc.write([]byte(p))
	}

	if len(dir.Children) > 0 {
		if dir.Name != "" || len(dir.Params) > 0 {
			enc.write([]byte(" "))
		}
		enc.write([]byte("{\n"))
		enc.push()
		enc.encodeBlock(dir.Children)
		enc.pop()
		enc.whdr()
		enc.write([]byte("}"))
	}
	enc.write([]byte("\n"))

	return enc.err
}
diff --git a/marshal_test.go b/marshal_test.go
new file mode 100644
index 0000000..03f5890
--- /dev/null
+++ b/marshal_test.go
@@ -0,0 +1,94 @@
package scfg

import (
	"testing"
)

func TestMarshal(t *testing.T) {
	for _, tc := range []struct {
		src  interface{}
		want string
	}{
		{
			src:  Block{},
			want: "",
		},
		{
			src: Block{{
				Children: Block{{
					Name:   "blk1",
					Params: []string{"p1", `"p2"`},
					Children: Block{
						{
							Name:   "sub1",
							Params: []string{"arg11", "arg12"},
						},
						{
							Name:   "sub2",
							Params: []string{"arg21", "arg22"},
						},
						{
							Name:   "sub3",
							Params: []string{"arg31", "arg32"},
							Children: Block{
								{
									Name: "sub-sub1",
								},
								{
									Name:   "sub-sub2",
									Params: []string{"arg321", "arg322"},
								},
							},
						},
					},
				}},
			}},
			want: `{
  blk1 p1 "p2" {
    sub1 arg11 arg12
    sub2 arg21 arg22
    sub3 arg31 arg32 {
      sub-sub1
      sub-sub2 arg321 arg322
    }
  }
}
`,
		},
		{
			src:  Directive{Name: "dir1"},
			want: "dir1\n",
		},
		{
			src:  Directive{Name: "dir1", Params: []string{"arg1", "arg2", `"arg3"`}},
			want: "dir1 arg1 arg2 \"arg3\"\n",
		},
		{
			src: Directive{
				Name: "dir1",
				Children: Block{
					{Name: "sub1"},
					{Name: "sub2", Params: []string{"arg1", "arg2"}},
				},
			},
			want: `dir1 {
  sub1
  sub2 arg1 arg2
}
`,
		},
	} {
		t.Run("", func(t *testing.T) {
			buf, err := Marshal(tc.src)
			if err != nil {
				t.Fatalf("could not marshal: %+v", err)
			}
			if got, want := string(buf), tc.want; got != want {
				t.Fatalf(
					"invalid marshal representation:\ngot:\n%s\nwant:\n%s\n---",
					got, want,
				)
			}
		})
	}
}
diff --git a/writer.go b/writer.go
new file mode 100644
index 0000000..7a7d603
--- /dev/null
+++ b/writer.go
@@ -0,0 +1,10 @@
package scfg

import "io"

// Write writes a parsed configuration to the provided io.Writer.
func Write(w io.Writer, blk Block) error {
	enc := NewEncoder(w)
	err := enc.encodeBlock(bl)
	return err
}
-- 
2.42.0
Thanks for the patch, I think it's a useful feature.