Sebastien Binet: 1 scfg: add Encoder and Write 3 files changed, 211 insertions(+), 0 deletions(-)
hi Simon,
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.
[...]
sure.
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 -3Learn more about email & git
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
Thanks for the patch, I think it's a useful feature.