cerrado @ 8a2461aa05895cc7828bc9619b50fa5dee5ed1f4

feat: Add config parsing
  1diff --git a/.gitignore b/.gitignore
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..e660fd93d3196215552065b1e63bf6a2f393ed86
  4--- /dev/null
  5+++ b/.gitignore
  6@@ -0,0 +1 @@
  7+bin/
  8diff --git a/Makefile b/Makefile
  9index 7a0e125baadd8ba25b560106a0b1bd91ec30df17..ecf165dce0342209fc2879c732b2eb8d7b06eba0 100644
 10--- a/Makefile
 11+++ b/Makefile
 12@@ -3,3 +3,6 @@ 	go build -o bin/cerrado
 13 
 14 run:
 15 	go run .
 16+
 17+test:
 18+	go test -v --tags=unit ./...
 19diff --git a/README.md b/README.md
 20new file mode 100644
 21index 0000000000000000000000000000000000000000..5a723e55161cbc997e4cbeec226fd7bd9555e258
 22--- /dev/null
 23+++ b/README.md
 24@@ -0,0 +1,3 @@
 25+# Cerrado
 26+
 27+Read only single user mail based forge for git.
 28diff --git a/config.example.scfg b/config.example.scfg
 29new file mode 100644
 30index 0000000000000000000000000000000000000000..eda4f38d14d06608e6447089bbf13c5416092d65
 31--- /dev/null
 32+++ b/config.example.scfg
 33@@ -0,0 +1,22 @@
 34+scan /srv/git/ {
 35+    public true
 36+}
 37+
 38+# TBD
 39+#user admin:iKlvHe1g0UoXE
 40+#
 41+#list main {
 42+#    server smtp.example.com
 43+#    user admin@admin.com
 44+#    password 1234567
 45+#    security tls
 46+#    authentication plain
 47+#    default false
 48+#}
 49+#
 50+#repository cerrado {
 51+#    title Cerrado
 52+#    description "Self host single person readonly forge"
 53+#    list main
 54+#    public true
 55+#}
 56diff --git a/go.mod b/go.mod
 57index cca006e89444dbdcf6c81c9f4f0f9acf6ed0bb6b..bfbe03ad8f3b8502d8d8f9ba9d3967552b1171e1 100644
 58--- a/go.mod
 59+++ b/go.mod
 60@@ -2,4 +2,8 @@ module git.gabrielgio.me/cerrado
 61 
 62 go 1.22.2
 63 
 64-require golang.org/x/sync v0.7.0 // indirect
 65+require (
 66+	git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
 67+	github.com/google/go-cmp v0.6.0
 68+	golang.org/x/sync v0.7.0
 69+)
 70diff --git a/go.sum b/go.sum
 71index e8ef4a360a1437536e8a3053e4ffc82fc69ce9c5..a98204454c0ef47546d171a1dada140655d0b45e 100644
 72--- a/go.sum
 73+++ b/go.sum
 74@@ -1,2 +1,8 @@
 75+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw=
 76+git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
 77+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 78+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 79+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 80+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 81 golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
 82 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 83diff --git a/main.go b/main.go
 84index 7c80564f57dfa106d4a4074f6c4586bc0a9c18ba..ba441fe71b8c6c0ea4aff7a8805d2e4729059e27 100644
 85--- a/main.go
 86+++ b/main.go
 87@@ -2,16 +2,20 @@ package main
 88 
 89 import (
 90 	"context"
 91+	"encoding/json"
 92+	"flag"
 93 	"log/slog"
 94 	"net/http"
 95 	"os"
 96 	"os/signal"
 97 	"time"
 98 
 99+	"git.gabrielgio.me/cerrado/pkg/config"
100 	"git.gabrielgio.me/cerrado/pkg/worker"
101 )
102 
103 func main() {
104+
105 	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
106 	defer stop()
107 	if err := run(ctx); err != nil {
108@@ -20,10 +24,36 @@ 	}
109 }
110 
111 func run(ctx context.Context) error {
112+	var (
113+		configPath = flag.String("config", "config.example.scfg", "File path for the configuration file")
114+	)
115+
116+	flag.Parse()
117+
118 	mux := http.NewServeMux()
119 	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
120-		if _, err := w.Write([]byte("Hello world!")); err != nil {
121+
122+		f, err := os.Open(*configPath)
123+		if err != nil {
124+			slog.Error("Error openning config file json", "error", err, "path", *configPath)
125+			return
126+		}
127+
128+		c, err := config.Parse(f)
129+		if err != nil {
130+			slog.Error("Error parsing config", "error", err, "path", *configPath)
131+			return
132+		}
133+
134+		b, err := json.MarshalIndent(c, "", "	")
135+		if err != nil {
136+			slog.Error("Error parsing json", "error", err)
137+			return
138+		}
139+
140+		if _, err := w.Write(b); err != nil {
141 			slog.Error("Error handling index", "error", err)
142+			return
143 		}
144 	})
145 	serverTask := worker.NewServerTask(&http.Server{Handler: mux, Addr: "0.0.0.0:8080"})
146diff --git a/pkg/config/config.go b/pkg/config/config.go
147new file mode 100644
148index 0000000000000000000000000000000000000000..ba1614f6fa318316db0b44a319846bf4a275bba6
149--- /dev/null
150+++ b/pkg/config/config.go
151@@ -0,0 +1,84 @@
152+package config
153+
154+import (
155+	"fmt"
156+	"io"
157+	"strconv"
158+
159+	"git.sr.ht/~emersion/go-scfg"
160+)
161+
162+type (
163+	Scan struct {
164+		Path   string
165+		Public bool
166+	}
167+
168+	Configuration struct {
169+		Scan *Scan
170+	}
171+)
172+
173+func Parse(r io.Reader) (*Configuration, error) {
174+	block, err := scfg.Read(r)
175+	if err != nil {
176+		return nil, err
177+	}
178+
179+	config := defaultConfiguration()
180+
181+	err = setScan(block, config.Scan)
182+	if err != nil {
183+		return nil, err
184+	}
185+
186+	return config, nil
187+}
188+
189+func defaultConfiguration() *Configuration {
190+	return &Configuration{
191+		Scan: &Scan{
192+			Public: true,
193+			Path:   "",
194+		},
195+	}
196+}
197+
198+func setScan(block scfg.Block, scan *Scan) error {
199+	scanDir := block.Get("scan")
200+	err := setString(scanDir, &scan.Path)
201+	if err != nil {
202+		return err
203+	}
204+
205+	public := scanDir.Children.Get("public")
206+	return setBool(public, &scan.Public)
207+}
208+
209+func setBool(dir *scfg.Directive, field *bool) error {
210+
211+	if dir != nil {
212+		p1 := first(dir.Params)
213+		v, err := strconv.ParseBool(p1)
214+		if err != nil {
215+			return fmt.Errorf("Error parsing bool param of %s: %w", dir.Name, err)
216+		}
217+		*field = v
218+	}
219+	return nil
220+}
221+
222+func setString(dir *scfg.Directive, field *string) error {
223+	if dir != nil {
224+		*field = first(dir.Params)
225+	}
226+	return nil
227+}
228+
229+func first[T any](v []T) T {
230+	if len(v) == 0 {
231+		var zero T
232+		return zero
233+	}
234+	return v[0]
235+}
236diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
237new file mode 100644
238index 0000000000000000000000000000000000000000..c8cd8876055e2cabe66e1ee574b5c36df358efcb
239--- /dev/null
240+++ b/pkg/config/config_test.go
241@@ -0,0 +1,56 @@
242+// go:build unit
243+package config
244+
245+import (
246+	"strings"
247+	"testing"
248+
249+	"github.com/google/go-cmp/cmp"
250+)
251+
252+func TestConfig(t *testing.T) {
253+	testCases := []struct {
254+		name           string
255+		config         string
256+		expectedConfig *Configuration
257+	}{
258+		{
259+			name:   "minimal scan",
260+			config: `scan "/srv/git"`,
261+			expectedConfig: &Configuration{
262+				Scan: &Scan{
263+					Public: true,
264+					Path:   "/srv/git",
265+				},
266+			},
267+		},
268+		{
269+			name: "complete scan",
270+			config: `scan "/srv/git" {
271+	public false
272+}`,
273+			expectedConfig: &Configuration{
274+				Scan: &Scan{
275+					Public: false,
276+					Path:   "/srv/git",
277+				},
278+			},
279+		},
280+	}
281+
282+	for _, tc := range testCases {
283+		t.Run(tc.name, func(t *testing.T) {
284+			r := strings.NewReader(tc.config)
285+			config, err := Parse(r)
286+			if err != nil {
287+				t.Fatalf("Error parsing config %s", err.Error())
288+			}
289+
290+			if diff := cmp.Diff(tc.expectedConfig, config); diff != "" {
291+				t.Errorf("Wrong result given - wanted + got\n %s", diff)
292+			}
293+
294+		})
295+
296+	}
297+}