Skip to content

Commit c7a5fd8

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

File tree

4 files changed

+426
-1
lines changed

4 files changed

+426
-1
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/linkedin/goavro/v2 v2.12.0
3030
github.com/marcboeker/go-duckdb v1.8.3
3131
github.com/pelletier/go-toml/v2 v2.2.0
32+
github.com/pkg/errors v0.9.1
3233
github.com/prometheus/client_golang v1.17.0
3334
github.com/riferrei/srclient v0.5.4
3435
github.com/santhosh-tekuri/jsonschema/v5 v5.2.0
@@ -106,7 +107,6 @@ require (
106107
github.com/mr-tron/base58 v1.2.0 // indirect
107108
github.com/oklog/run v1.0.0 // indirect
108109
github.com/pierrec/lz4/v4 v4.1.21 // indirect
109-
github.com/pkg/errors v0.9.1 // indirect
110110
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
111111
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
112112
github.com/prometheus/common v0.44.0 // indirect

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

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package configtest
2+
3+
import (
4+
"encoding"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"strings"
9+
"testing"
10+
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+
func AssertFieldsNotNil(t *testing.T, s interface{}) {
20+
err := assertValNotNil(t, "", reflect.ValueOf(s))
21+
_, err = config.MultiErrorList(err)
22+
assert.NoError(t, err)
23+
}
24+
25+
// assertFieldsNotNil recursively checks the struct s for nil fields.
26+
func assertFieldsNotNil(t *testing.T, prefix string, s reflect.Value) (err error) {
27+
t.Helper()
28+
require.Equal(t, reflect.Struct, s.Kind())
29+
30+
typ := s.Type()
31+
for i := 0; i < s.NumField(); i++ {
32+
f := s.Field(i)
33+
key := prefix
34+
if tf := typ.Field(i); !tf.Anonymous {
35+
if key != "" {
36+
key += "."
37+
}
38+
key += tf.Name
39+
}
40+
err = multierr.Combine(err, assertValNotNil(t, key, f))
41+
}
42+
return
43+
}
44+
45+
// assertValuesNotNil recursively checks the map m for nil values.
46+
func assertValuesNotNil(t *testing.T, prefix string, m reflect.Value) (err error) {
47+
t.Helper()
48+
require.Equal(t, reflect.Map, m.Kind())
49+
if prefix != "" {
50+
prefix += "."
51+
}
52+
53+
mi := m.MapRange()
54+
for mi.Next() {
55+
key := prefix + mi.Key().String()
56+
err = multierr.Combine(err, assertValNotNil(t, key, mi.Value()))
57+
}
58+
return
59+
}
60+
61+
// assertElementsNotNil recursively checks the slice s for nil values.
62+
func assertElementsNotNil(t *testing.T, prefix string, s reflect.Value) (err error) {
63+
t.Helper()
64+
require.Equal(t, reflect.Slice, s.Kind())
65+
66+
for i := 0; i < s.Len(); i++ {
67+
err = multierr.Combine(err, assertValNotNil(t, prefix, s.Index(i)))
68+
}
69+
return
70+
}
71+
72+
var (
73+
textUnmarshaler encoding.TextUnmarshaler
74+
textUnmarshalerType = reflect.TypeOf(&textUnmarshaler).Elem()
75+
)
76+
77+
// assertValNotNil recursively checks that val is not nil. val must be a struct, map, slice, or point to one.
78+
func assertValNotNil(t *testing.T, key string, val reflect.Value) error {
79+
t.Helper()
80+
k := val.Kind()
81+
switch k { //nolint:exhaustive
82+
case reflect.Ptr:
83+
if val.IsNil() {
84+
return fmt.Errorf("%s: nil", key)
85+
}
86+
}
87+
if k == reflect.Ptr {
88+
if val.Type().Implements(textUnmarshalerType) {
89+
return nil // skip values unmarshaled from strings
90+
}
91+
val = val.Elem()
92+
}
93+
switch val.Kind() {
94+
case reflect.Struct:
95+
if val.Type().Implements(textUnmarshalerType) {
96+
return nil // skip values unmarshaled from strings
97+
}
98+
return assertFieldsNotNil(t, key, val)
99+
case reflect.Map:
100+
if val.IsNil() {
101+
return nil // not actually a problem
102+
}
103+
return assertValuesNotNil(t, key, val)
104+
case reflect.Slice:
105+
if val.IsNil() {
106+
return nil // not actually a problem
107+
}
108+
return assertElementsNotNil(t, key, val)
109+
default:
110+
return nil
111+
}
112+
}
113+
114+
func TestDoc[C any](t *testing.T, docsTOML string) {
115+
t.Helper()
116+
var c C
117+
err := config.DecodeTOML(strings.NewReader(docsTOML), &c)
118+
var strict *toml.StrictMissingError
119+
if err != nil && strings.Contains(err.Error(), "undecoded keys: ") {
120+
t.Errorf("Docs contain extra fields: %v", err)
121+
} else if errors.As(err, &strict) {
122+
t.Fatal("StrictMissingError:", strict.String())
123+
} else {
124+
require.NoError(t, err)
125+
}
126+
AssertFieldsNotNil(t, c)
127+
}

0 commit comments

Comments
 (0)