From 79ba0b2dc79c74a4e4f0755636f2532fd6c161ac Mon Sep 17 00:00:00 2001
From: feliixx <peteladrien@gmail.com>
Date: Thu, 5 May 2022 16:03:51 -1000
Subject: [PATCH] add support for JSONC in config

---
 README.md      |  2 +-
 parser.go      | 66 +++++++++++++++++++++++++++++++++++++++++++++++---
 parser_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 122 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md
index 76a730c..7cb8331 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A small configuration library for go application with a viper-like API, but with
 
 It supports: 
 
-  * reading a config in JSON format 
+  * reading a config in JSON or JSONC ( JSON with comments) 
   * setting default 
 
 
diff --git a/parser.go b/parser.go
index fece6db..405d72f 100644
--- a/parser.go
+++ b/parser.go
@@ -1,6 +1,7 @@
 package boa
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -15,15 +16,27 @@ var (
 )
 
 // ParseConfig reads the config from an io.Reader.
-func ParseConfig(jsonConfig io.Reader) error {
+//
+// The config may be in JSON or in JSONC ( json with comment)
+// Allowed format for comments are 
+//  * single line ( //... )
+//  * multiligne ( /*...*/ )
+func ParseConfig(jsoncConfig io.Reader) error {
+
+	jsonc, err := io.ReadAll(jsoncConfig)
+	if err != nil {
+		return fmt.Errorf("fail to read from reader: %v", err)
+	}
+
+	cleanJson := removeComment(jsonc)
 
-	d := json.NewDecoder(jsonConfig)
+	d := json.NewDecoder(bytes.NewReader(cleanJson))
 	d.UseNumber()
 
 	var src map[string]any
-	err := d.Decode(&src)
+	err = d.Decode(&src)
 	if err != nil {
-		return err
+		return fmt.Errorf("fail to parse JSON: %v", err)
 	}
 
 	flatten("", src, config)
@@ -31,6 +44,51 @@ func ParseConfig(jsonConfig io.Reader) error {
 	return nil
 }
 
+func removeComment(src []byte) []byte {
+
+	output := make([]byte, 0, len(src))
+
+	var prev byte
+	inString := false
+	inSingleLineComment := false
+	inMultiligneComment := false
+
+	for _, b := range src {
+
+		switch b {
+
+		case '"':
+			inString = !inString
+		case '/':
+			if !inString && prev == '/' {
+				output = output[0 : len(output)-1]
+				inSingleLineComment = true
+			}
+
+			if inMultiligneComment && prev == '*' {
+				inMultiligneComment = false
+				continue
+			}
+
+		case '*':
+			if !inString && prev == '/' {
+				output = output[0 : len(output)-1]
+				inMultiligneComment = true
+			}
+		case '\n':
+			inSingleLineComment = false
+		}
+
+		prev = b
+
+		if !inSingleLineComment && !inMultiligneComment {
+			output = append(output, b)
+		}
+	}
+
+	return output
+}
+
 func flatten(prefix string, src map[string]any, dst map[string]any) {
 
 	if len(prefix) > 0 {
diff --git a/parser_test.go b/parser_test.go
index 399dc5a..0050ddd 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -308,6 +308,65 @@ func TestSetDefaults(t *testing.T) {
 	}
 }
 
+func TestRemoveComment(t *testing.T) {
+
+	config := `
+	{
+		/* some 
+		multiline 
+		comment */
+		"smtp": {
+			// single line 
+			"enabled": true, // with trailing space
+			"host": "http://127.0.0.1",// without trailing space
+			"port": 55,
+			"pwd": "fhd/*|,;,bdo*/"
+		} 			/**/
+
+
+	}
+	// `
+
+	loadConfig(t, config)
+
+	tests := []struct {
+		name string
+		want any
+		got  any
+	}{
+		{
+			name: "smtp.enabled",
+			want: true,
+			got:  boa.GetBool("smtp.enabled"),
+		},
+		{
+			name: "smtp.host",
+			want: "http://127.0.0.1",
+			got:  boa.GetString("smtp.host"),
+		},
+		{
+			name: "smtp.port",
+			want: 55,
+			got:  boa.GetInt("smtp.port"),
+		},
+		{
+			name: "smtp.pwd",
+			want: "fhd/*|,;,bdo*/",
+			got:  boa.GetString("smtp.pwd"),
+		},
+	}
+
+	for _, test := range tests {
+
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.want != tt.got {
+				t.Errorf("expected '%v' but got '%v'", tt.want, tt.got)
+			}
+		})
+	}
+}
+
 func loadConfig(t *testing.T, config string) {
 	err := boa.ParseConfig(strings.NewReader(config))
 	if err != nil {