~emersion/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
4 2

[PATCH go-scfg] scfg: add Encoder and Write

Details
Message ID
<20230915130443.1486422-1-s@sbinet.org>
DKIM signature
missing
Download raw message
Patch: +211 -0
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
Details
Message ID
<NFWucmjpeeVCPTPz0I9i5OnZB-laAOFxZA9mubdS_DFyk2UddNLeicr35DPCKfgIMa3GSPd7Y3VutcAuYehFy7HijZ1QEQ6XNCNSz9jKQX0=@emersion.fr>
In-Reply-To
<20230915130443.1486422-1-s@sbinet.org> (view parent)
DKIM signature
missing
Download raw message
Thanks for the patch, I think it's a useful feature.

On Friday, September 15th, 2023 at 15:04, Sebastien Binet <s@sbinet.org> wrote:

> 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) {

Do we really need to use interface{} here? What is the use-case for passing a
Directive?

More generally, I'd prefer to reserve Marshal/Encoder abstractions for
formatting scfg from arbitrary Go types, such as done in [1].

[1]: https://lists.sr.ht/~emersion/public-inbox/patches/42271

> +	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("  "),

Can we use tabs instead?

> +	}
> +}
> +
> +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))

I don't think writing the name is quite enough: we need to quote the string if
it contains special characters (DQUOTE, "\", LF, "'", "{", "}", WSP).

> +	if dir.Name != "" && len(dir.Params) > 0 {

The name cannot be empty. Maybe return an error in this case?

> +		enc.write([]byte(" "))
> +	}
> +	for i, p := range dir.Params {
> +		if i > 0 {
> +			enc.write([]byte(" "))
> +		}
> +		enc.write([]byte(p))

Ditto: we need to quote here.

> +	}
> +
> +	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
Details
Message ID
<CVZJ2I1CFUR8.5OQA5YOLF9AQ@sbinet.org>
In-Reply-To
<NFWucmjpeeVCPTPz0I9i5OnZB-laAOFxZA9mubdS_DFyk2UddNLeicr35DPCKfgIMa3GSPd7Y3VutcAuYehFy7HijZ1QEQ6XNCNSz9jKQX0=@emersion.fr> (view parent)
DKIM signature
missing
Download raw message
hi Simon,

>> // Marshal returns the SCFG encoding of v.
>> func Marshal(v interface{}) ([]byte, error) {
>
> Do we really need to use interface{} here? What is the use-case for passing a
> Directive?
> 
> More generally, I'd prefer to reserve Marshal/Encoder abstractions for
> formatting scfg from arbitrary Go types, such as done in [1].
> 
> [1]: https://lists.sr.ht/~emersion/public-inbox/patches/42271

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)
)
```

[...]

>> // NewEncoder returns a new encoder that writes to w.
>> func NewEncoder(w io.Writer) *Encoder {
>> 	return &Encoder{
>> 		w:   w,
>> 		ind: []byte("  "),
>
> Can we use tabs instead?

sure.

>> func (enc *Encoder) encodeDir(dir Directive) error {
>> 	enc.whdr()
>> 	enc.write([]byte(dir.Name))

> I don't think writing the name is quite enough: we need to quote the string if
> it contains special characters (DQUOTE, "\", LF, "'", "{", "}", WSP).

right.

v2 coming up.

-s
Details
Message ID
<HvVvNaaXR3aeG0uQiGwEP9EwOb6D1Pm4DU5xiYxKFQDhtn2-j-eOy1-YNT33cFUG17VtQYJqbBGtvSqnQvuclu8uC5eGr5QYewB450PbvEs=@emersion.fr>
In-Reply-To
<CVZJ2I1CFUR8.5OQA5YOLF9AQ@sbinet.org> (view parent)
DKIM signature
missing
Download raw message
On Wednesday, October 4th, 2023 at 10:55, Sebastien Binet <s@sbinet.org> wrote:

> hi Simon,
> 
> >> // Marshal returns the SCFG encoding of v.
> >> func Marshal(v interface{}) ([]byte, error) {
> >
> > Do we really need to use interface{} here? What is the use-case for passing a
> > Directive?
> >
> > More generally, I'd prefer to reserve Marshal/Encoder abstractions for
> > formatting scfg from arbitrary Go types, such as done in [1].
> >
> > [1]: https://lists.sr.ht/~emersion/public-inbox/patches/42271
> 
> 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)
> )
> ```

That makes sense! That said, I'd still prefer to hold off exposing a public API
until we actually have a use for it. In other words, I'd prefer to only expose
Write for now, and add Marshal/Encoder when we actually implement the new
functionality.
Details
Message ID
<CVZJF56Z92OM.2MW9WMNGPZCL1@sbinet.org>
In-Reply-To
<HvVvNaaXR3aeG0uQiGwEP9EwOb6D1Pm4DU5xiYxKFQDhtn2-j-eOy1-YNT33cFUG17VtQYJqbBGtvSqnQvuclu8uC5eGr5QYewB450PbvEs=@emersion.fr> (view parent)
DKIM signature
missing
Download raw message
> That makes sense! That said, I'd still prefer to hold off exposing a public API
> until we actually have a use for it. In other words, I'd prefer to only expose
> Write for now, and add Marshal/Encoder when we actually implement the new
> functionality.

ok. I'll send a v3, then.
Reply to thread Export thread (mbox)