Skip to content

Commit 7c113b3

Browse files
mover config resolvers to cldf (#316)
1 parent fb41871 commit 7c113b3

File tree

3 files changed

+557
-0
lines changed

3 files changed

+557
-0
lines changed

.changeset/lovely-berries-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Move config resolver framework into CLDF

changeset/resolvers/resolver.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package resolvers
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"runtime"
9+
"sort"
10+
"sync"
11+
)
12+
13+
// ConfigResolverManager manages config resolvers with thread-safe operations
14+
type ConfigResolverManager struct {
15+
mu sync.RWMutex
16+
byName map[string]registered // name → {fn, info}
17+
}
18+
19+
type registered struct {
20+
fn ConfigResolver
21+
info ResolverInfo
22+
}
23+
24+
// NewConfigResolverManager creates a new ConfigResolverManager
25+
func NewConfigResolverManager() *ConfigResolverManager {
26+
return &ConfigResolverManager{
27+
byName: map[string]registered{},
28+
}
29+
}
30+
31+
// Register binds an explicit name to a resolver and stores its metadata.
32+
// It panics if the name is already taken.
33+
func (m *ConfigResolverManager) Register(
34+
fn ConfigResolver,
35+
info ResolverInfo,
36+
) {
37+
name := extractFunctionName(fn)
38+
39+
if name == "" {
40+
panic("resolver name must not be empty")
41+
}
42+
43+
m.mu.Lock()
44+
defer m.mu.Unlock()
45+
46+
if _, dup := m.byName[name]; dup {
47+
panic(fmt.Sprintf("resolver %q already registered", name))
48+
}
49+
50+
// Signature check and type discovery
51+
rf := reflect.TypeOf(fn)
52+
if rf.Kind() != reflect.Func || rf.NumIn() != 1 || rf.NumOut() != 2 ||
53+
!rf.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
54+
panic(fmt.Sprintf(
55+
"resolver %q must be func(<In>) (<Out>, error) – got %s", name, rf,
56+
))
57+
}
58+
59+
m.byName[name] = registered{fn: fn, info: info}
60+
}
61+
62+
// NameOf returns the registered name for the given resolver, or empty string.
63+
func (m *ConfigResolverManager) NameOf(r ConfigResolver) string {
64+
m.mu.RLock()
65+
defer m.mu.RUnlock()
66+
67+
// Generate the name consistently
68+
name := extractFunctionName(r)
69+
70+
// Verify this function is actually registered with this manager
71+
if registered, exists := m.byName[name]; exists {
72+
// Double-check it's the same function by comparing pointers
73+
if reflect.ValueOf(registered.fn).Pointer() == reflect.ValueOf(r).Pointer() {
74+
return name
75+
}
76+
}
77+
78+
return ""
79+
}
80+
81+
// ListResolvers returns all registered names in deterministic order.
82+
func (m *ConfigResolverManager) ListResolvers() []string {
83+
m.mu.RLock()
84+
defer m.mu.RUnlock()
85+
names := make([]string, 0, len(m.byName))
86+
for n := range m.byName {
87+
names = append(names, n)
88+
}
89+
sort.Strings(names)
90+
91+
return names
92+
}
93+
94+
// extractFunctionName extracts the full qualified function name (with package path) from a function using reflection
95+
// This is used as the key to avoid naming collisions between packages
96+
func extractFunctionName(fn ConfigResolver) string {
97+
// Get the full function name with package path
98+
fullName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
99+
100+
if fullName == "" {
101+
return "unknown_resolver"
102+
}
103+
104+
return fullName
105+
}
106+
107+
// ConfigResolver can be *any* function whose signature is:
108+
//
109+
// func(<Input>) (<Output>, error)
110+
//
111+
// The concrete types are discovered at runtime via reflection. Signature check happens at registration time.
112+
type ConfigResolver any
113+
114+
// ResolverInfo contains metadata about a config resolver
115+
type ResolverInfo struct {
116+
Description string
117+
ExampleYAML string
118+
}
119+
120+
// CallResolver unmarshals raw JSON into the input type expected by `resolver`,
121+
// invokes it, and converts the first return value to the requested generic
122+
// type C.
123+
func CallResolver[C any](resolver ConfigResolver, payload json.RawMessage) (C, error) {
124+
var zero C
125+
126+
rVal := reflect.ValueOf(resolver)
127+
rType := rVal.Type() // func(<In>) (<Out>, error)
128+
129+
// Basic validation (double check – already done at registration time).
130+
if rType.Kind() != reflect.Func || rType.NumIn() != 1 || rType.NumOut() != 2 ||
131+
!rType.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
132+
return zero, errors.New("resolver must be func(<In>) (<Out>, error)")
133+
}
134+
135+
inType := rType.In(0)
136+
137+
// Allocate a new value of the required input type and unmarshal into it.
138+
var arg reflect.Value
139+
if inType.Kind() == reflect.Ptr {
140+
// If the function expects a pointer, create the underlying type and get a pointer to it
141+
elemType := inType.Elem()
142+
elemPtr := reflect.New(elemType)
143+
if err := json.Unmarshal(payload, elemPtr.Interface()); err != nil {
144+
return zero, fmt.Errorf("unmarshal payload into %v: %w", inType, err)
145+
}
146+
arg = elemPtr
147+
} else {
148+
// If the function expects a value, create a pointer, unmarshal, then get the value
149+
inPtr := reflect.New(inType)
150+
if err := json.Unmarshal(payload, inPtr.Interface()); err != nil {
151+
return zero, fmt.Errorf("unmarshal payload into %v: %w", inType, err)
152+
}
153+
arg = inPtr.Elem()
154+
}
155+
156+
// Invoke the resolver.
157+
outs := rVal.Call([]reflect.Value{arg})
158+
if errIface := outs[1].Interface(); errIface != nil {
159+
return zero, errIface.(error)
160+
}
161+
162+
// Convert the first return value to C.
163+
outIface := outs[0].Interface()
164+
cfg, ok := outIface.(C)
165+
if !ok {
166+
return zero, fmt.Errorf("resolver returned %T, expected %T", outIface, zero)
167+
}
168+
169+
return cfg, nil
170+
}

0 commit comments

Comments
 (0)