Skip to content

Commit d616f99

Browse files
committed
pkg/config/configtest: move package cfgtest from chainlink/v2
1 parent dfdf960 commit d616f99

File tree

6 files changed

+441
-1
lines changed

6 files changed

+441
-1
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/jmoiron/sqlx v1.4.0
2626
github.com/jonboulle/clockwork v0.4.0
2727
github.com/jpillora/backoff v1.0.0
28+
github.com/kylelemons/godebug v1.1.0
2829
github.com/lib/pq v1.10.9
2930
github.com/linkedin/goavro/v2 v2.12.0
3031
github.com/marcboeker/go-duckdb v1.8.3

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
229229
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
230230
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
231231
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
232+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
233+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
232234
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
233235
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
234236
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=

pkg/config/configdoc/configdoc.go

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package configdoc
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"go.uber.org/multierr"
8+
9+
"github.com/smartcontractkit/chainlink-common/pkg/config"
10+
)
11+
12+
// Generate returns MarkDown documentation generated from the TOML string.
13+
// TODO doc more; advanced; extended on root level only
14+
func Generate(toml, header, example string, extendedDescriptions map[string]string) (string, error) {
15+
items, err := parseTOMLDocs(toml, extendedDescriptions)
16+
var sb strings.Builder
17+
18+
sb.WriteString(header)
19+
sb.WriteString(`
20+
## Example
21+
22+
`)
23+
sb.WriteString("```toml\n")
24+
sb.WriteString(example)
25+
sb.WriteString("```\n\n")
26+
27+
for _, item := range items {
28+
sb.WriteString(item.String())
29+
sb.WriteString("\n\n")
30+
}
31+
32+
return sb.String(), err
33+
}
34+
35+
const (
36+
fieldDefault = "# Default"
37+
fieldExample = "# Example"
38+
39+
tokenAdvanced = "**ADVANCED**"
40+
)
41+
42+
func advancedWarning(msg string) string {
43+
return fmt.Sprintf(":warning: **_ADVANCED_**: _%s_\n", msg)
44+
}
45+
46+
// lines holds a set of contiguous lines
47+
type lines []string
48+
49+
func (d lines) String() string {
50+
return strings.Join(d, "\n")
51+
}
52+
53+
type table struct {
54+
name string
55+
codes lines
56+
adv bool
57+
desc lines
58+
extended string
59+
}
60+
61+
func newTable(line string, desc lines, extendedDescriptions map[string]string) *table {
62+
t := &table{
63+
name: strings.Trim(line, "[]"),
64+
codes: []string{line},
65+
desc: desc,
66+
}
67+
if len(desc) > 0 {
68+
if strings.HasPrefix(strings.TrimSpace(desc[0]), tokenAdvanced) {
69+
t.adv = true
70+
t.desc = t.desc[1:]
71+
} else if extended, ok := extendedDescriptions[t.name]; ok {
72+
t.extended = extended
73+
}
74+
}
75+
return t
76+
}
77+
78+
func newArrayOfTables(line string, desc lines, extendedDescriptions map[string]string) *table {
79+
t := &table{
80+
name: strings.Trim(strings.Trim(line, fieldExample), "[]"),
81+
codes: []string{line},
82+
desc: desc,
83+
}
84+
if len(desc) > 0 {
85+
if strings.HasPrefix(strings.TrimSpace(desc[0]), tokenAdvanced) {
86+
t.adv = true
87+
t.desc = t.desc[1:]
88+
} else if extended, ok := extendedDescriptions[t.name]; ok {
89+
t.extended = extended
90+
}
91+
}
92+
return t
93+
}
94+
95+
func (t table) advanced() string {
96+
if t.adv {
97+
return advancedWarning("Do not change these settings unless you know what you are doing.")
98+
}
99+
return ""
100+
}
101+
102+
func (t table) code() string {
103+
if t.extended == "" {
104+
return fmt.Sprint("```toml\n", t.codes, "\n```\n")
105+
}
106+
return ""
107+
}
108+
109+
// String prints a table as an H2, followed by a code block and description.
110+
func (t *table) String() string {
111+
return fmt.Sprint("## ", t.name, "\n",
112+
t.advanced(),
113+
t.code(),
114+
t.desc,
115+
t.extended)
116+
}
117+
118+
type keyval struct {
119+
name string
120+
code string
121+
adv bool
122+
desc lines
123+
}
124+
125+
func newKeyval(line string, desc lines) keyval {
126+
line = strings.TrimSpace(line)
127+
kv := keyval{
128+
name: line[:strings.Index(line, " ")],
129+
code: line,
130+
desc: desc,
131+
}
132+
if len(desc) > 0 && strings.HasPrefix(strings.TrimSpace(desc[0]), tokenAdvanced) {
133+
kv.adv = true
134+
kv.desc = kv.desc[1:]
135+
}
136+
return kv
137+
}
138+
139+
func (k keyval) advanced() string {
140+
if k.adv {
141+
return advancedWarning("Do not change this setting unless you know what you are doing.")
142+
}
143+
return ""
144+
}
145+
146+
// String prints a keyval as an H3, followed by a code block and description.
147+
func (k keyval) String() string {
148+
name := k.name
149+
if i := strings.LastIndex(name, "."); i > -1 {
150+
name = name[i+1:]
151+
}
152+
return fmt.Sprint("### ", name, "\n",
153+
k.advanced(),
154+
"```toml\n",
155+
k.code,
156+
"\n```\n",
157+
k.desc)
158+
}
159+
160+
func parseTOMLDocs(s string, extendedDescriptions map[string]string) (items []fmt.Stringer, err error) {
161+
defer func() { _, err = config.MultiErrorList(err) }()
162+
globalTable := table{name: "Global"}
163+
currentTable := &globalTable
164+
items = append(items, currentTable)
165+
var desc lines
166+
for _, line := range strings.Split(s, "\n") {
167+
if strings.HasPrefix(line, "#") {
168+
// comment
169+
desc = append(desc, strings.TrimSpace(line[1:]))
170+
} else if strings.TrimSpace(line) == "" {
171+
// empty
172+
if len(desc) > 0 {
173+
items = append(items, desc)
174+
desc = nil
175+
}
176+
} else if strings.HasPrefix(line, "[[") {
177+
currentTable = newArrayOfTables(line, desc, extendedDescriptions)
178+
items = append(items, currentTable)
179+
desc = nil
180+
} else if strings.HasPrefix(line, "[") {
181+
currentTable = newTable(line, desc, extendedDescriptions)
182+
items = append(items, currentTable)
183+
desc = nil
184+
} else {
185+
kv := newKeyval(line, desc)
186+
shortName := kv.name
187+
if currentTable != &globalTable {
188+
// update to full name
189+
kv.name = currentTable.name + "." + kv.name
190+
}
191+
if len(kv.desc) == 0 {
192+
err = multierr.Append(err, fmt.Errorf("%s: missing description", kv.name)) //TODO errors.Join
193+
} else if !strings.HasPrefix(kv.desc[0], shortName) {
194+
err = multierr.Append(err, fmt.Errorf("%s: description does not begin with %q", kv.name, shortName))
195+
}
196+
if !strings.HasSuffix(line, fieldDefault) && !strings.HasSuffix(line, fieldExample) {
197+
err = multierr.Append(err, fmt.Errorf(`%s: is not one of %v`, kv.name, []string{fieldDefault, fieldExample}))
198+
}
199+
200+
items = append(items, kv)
201+
currentTable.codes = append(currentTable.codes, kv.code)
202+
desc = nil
203+
}
204+
}
205+
if len(globalTable.codes) == 0 {
206+
// drop it
207+
items = items[1:]
208+
}
209+
if len(desc) > 0 {
210+
items = append(items, desc)
211+
}
212+
return
213+
}

pkg/config/configtest/configtest.go

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package configtest
2+
3+
import (
4+
"encoding"
5+
"fmt"
6+
"reflect"
7+
"strings"
8+
"testing"
9+
10+
"github.com/kylelemons/godebug/diff"
11+
"github.com/pelletier/go-toml/v2"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/multierr"
15+
16+
"github.com/smartcontractkit/chainlink-common/pkg/config"
17+
)
18+
19+
// AssertFieldsNotNil recursively checks s for nil fields. s must be a struct.
20+
func AssertFieldsNotNil(t *testing.T, s interface{}) {
21+
t.Helper()
22+
err := assertValNotNil(t, "", reflect.ValueOf(s))
23+
_, err = config.MultiErrorList(err)
24+
assert.NoError(t, err)
25+
}
26+
27+
// assertFieldsNotNil recursively checks the struct s for nil fields.
28+
func assertFieldsNotNil(t *testing.T, prefix string, s reflect.Value) (err error) {
29+
t.Helper()
30+
require.Equal(t, reflect.Struct, s.Kind())
31+
32+
typ := s.Type()
33+
for i := 0; i < s.NumField(); i++ {
34+
f := s.Field(i)
35+
key := prefix
36+
if tf := typ.Field(i); !tf.Anonymous {
37+
if key != "" {
38+
key += "."
39+
}
40+
key += tf.Name
41+
}
42+
err = multierr.Combine(err, assertValNotNil(t, key, f)) //TODO errors.Join
43+
}
44+
return
45+
}
46+
47+
// assertValuesNotNil recursively checks the map m for nil values.
48+
func assertValuesNotNil(t *testing.T, prefix string, m reflect.Value) (err error) {
49+
t.Helper()
50+
require.Equal(t, reflect.Map, m.Kind())
51+
if prefix != "" {
52+
prefix += "."
53+
}
54+
55+
mi := m.MapRange()
56+
for mi.Next() {
57+
key := prefix + mi.Key().String()
58+
err = multierr.Combine(err, assertValNotNil(t, key, mi.Value()))
59+
}
60+
return
61+
}
62+
63+
// assertElementsNotNil recursively checks the slice s for nil values.
64+
func assertElementsNotNil(t *testing.T, prefix string, s reflect.Value) (err error) {
65+
t.Helper()
66+
require.Equal(t, reflect.Slice, s.Kind())
67+
68+
for i := 0; i < s.Len(); i++ {
69+
err = multierr.Combine(err, assertValNotNil(t, prefix, s.Index(i)))
70+
}
71+
return
72+
}
73+
74+
var (
75+
textUnmarshaler encoding.TextUnmarshaler
76+
textUnmarshalerType = reflect.TypeOf(&textUnmarshaler).Elem()
77+
)
78+
79+
// assertValNotNil recursively checks that val is not nil. val must be a struct, map, slice, or point to one.
80+
func assertValNotNil(t *testing.T, key string, val reflect.Value) error {
81+
t.Helper()
82+
k := val.Kind()
83+
switch k { //nolint:exhaustive
84+
case reflect.Ptr:
85+
if val.IsNil() {
86+
return fmt.Errorf("%s: nil", key)
87+
}
88+
}
89+
if k == reflect.Ptr {
90+
if val.Type().Implements(textUnmarshalerType) {
91+
return nil // skip values unmarshaled from strings
92+
}
93+
val = val.Elem()
94+
}
95+
switch val.Kind() {
96+
case reflect.Struct:
97+
if val.Type().Implements(textUnmarshalerType) {
98+
return nil // skip values unmarshaled from strings
99+
}
100+
return assertFieldsNotNil(t, key, val)
101+
case reflect.Map:
102+
if val.IsNil() {
103+
return nil // not actually a problem
104+
}
105+
return assertValuesNotNil(t, key, val)
106+
case reflect.Slice:
107+
if val.IsNil() {
108+
return nil // not actually a problem
109+
}
110+
return assertElementsNotNil(t, key, val)
111+
default:
112+
return nil
113+
}
114+
}
115+
116+
// AssertDocsTOMLComplete ensures that docsTOML decodes to C, without any unknown fields and without leaving any nil.
117+
func AssertDocsTOMLComplete[C any](t *testing.T, docsTOML string) {
118+
t.Helper()
119+
var c C
120+
err := config.DecodeTOML(strings.NewReader(docsTOML), &c)
121+
if err != nil && strings.Contains(err.Error(), "undecoded keys: ") {
122+
t.Errorf("Docs contain extra fields: %v", err)
123+
} else {
124+
require.NoError(t, err)
125+
}
126+
AssertFieldsNotNil(t, c)
127+
}
128+
129+
// TestFullMarshal ensures that c has no nil fields, and that it encodes to expTOML.
130+
func TestFullMarshal[C any](t *testing.T, c C, expTOML string) {
131+
t.Helper()
132+
AssertFieldsNotNil(t, c)
133+
134+
b, err := toml.Marshal(c)
135+
require.NoError(t, err)
136+
s := string(b)
137+
t.Log(s)
138+
assert.Equal(t, expTOML, s, diff.Diff(expTOML, s))
139+
}

0 commit comments

Comments
 (0)