Skip to content

Commit 055ee4b

Browse files
committed
updated openapi schema builder
1 parent 1da05d0 commit 055ee4b

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package compile
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"strings"
10+
11+
"github.com/viant/datly/repository/shape"
12+
dqlpre "github.com/viant/datly/repository/shape/dql/preprocess"
13+
)
14+
15+
// RouteIndexEntry maps one source DQL file to one concrete method+URI route key.
16+
type RouteIndexEntry struct {
17+
RouteKey string
18+
Method string
19+
URI string
20+
SourcePath string
21+
Namespace string
22+
}
23+
24+
// RouteIndex stores source-to-route mapping and lookup structures.
25+
type RouteIndex struct {
26+
ByRouteKey map[string]*RouteIndexEntry
27+
ByNamespace map[string][]*RouteIndexEntry
28+
Conflicts map[string][]string
29+
}
30+
31+
// BuildRouteIndex scans DQL files and builds route-key mapping.
32+
func BuildRouteIndex(paths []string, opts ...shape.CompileOption) (*RouteIndex, error) {
33+
compileOptions := applyCompileOptions(opts)
34+
layout := newCompilePathLayout(compileOptions)
35+
index := &RouteIndex{
36+
ByRouteKey: map[string]*RouteIndexEntry{},
37+
ByNamespace: map[string][]*RouteIndexEntry{},
38+
Conflicts: map[string][]string{},
39+
}
40+
if len(paths) == 0 {
41+
return index, nil
42+
}
43+
normalized := make([]string, 0, len(paths))
44+
for _, item := range paths {
45+
item = strings.TrimSpace(item)
46+
if item == "" {
47+
continue
48+
}
49+
normalized = append(normalized, item)
50+
}
51+
sort.Strings(normalized)
52+
53+
for _, sourcePath := range normalized {
54+
data, err := os.ReadFile(sourcePath)
55+
if err != nil {
56+
return nil, fmt.Errorf("route index: unable to read %s: %w", sourcePath, err)
57+
}
58+
sourceName := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
59+
dql := string(data)
60+
_, _, directives, _ := dqlpre.Extract(dql)
61+
source := &shape.Source{
62+
Name: sourceName,
63+
Path: sourcePath,
64+
DQL: dql,
65+
}
66+
settings := extractRuleSettings(source, directives)
67+
namespace, _ := dqlToRouteNamespaceWithLayout(sourcePath, layout)
68+
uri := strings.TrimSpace(settings.URI)
69+
if uri == "" {
70+
uri = inferDefaultURI(namespace)
71+
}
72+
if uri == "" {
73+
continue
74+
}
75+
methods := parseRouteMethods(settings.Method)
76+
for _, method := range methods {
77+
entry := &RouteIndexEntry{
78+
Method: method,
79+
URI: normalizeURI(uri),
80+
SourcePath: sourcePath,
81+
Namespace: namespace,
82+
}
83+
entry.RouteKey = normalizeRouteKey(entry.Method, entry.URI)
84+
index.addEntry(entry)
85+
}
86+
}
87+
return index, nil
88+
}
89+
90+
// Resolve maps a component reference from current source context to route key.
91+
// It returns false when route cannot be resolved deterministically.
92+
func (r *RouteIndex) Resolve(ref, currentSource string, opts ...shape.CompileOption) (string, bool) {
93+
if r == nil {
94+
return "", false
95+
}
96+
method, value := splitRouteKey(ref)
97+
value = strings.TrimSpace(value)
98+
if value == "" {
99+
return "", false
100+
}
101+
layout := newCompilePathLayout(applyCompileOptions(opts))
102+
routeKeyFromURI := func(uri string) (string, bool) {
103+
key := normalizeRouteKey(method, uri)
104+
if _, conflicted := r.Conflicts[key]; conflicted {
105+
return "", false
106+
}
107+
if _, ok := r.ByRouteKey[key]; !ok {
108+
return "", false
109+
}
110+
return key, true
111+
}
112+
113+
if strings.HasPrefix(value, "/v1/api/") || strings.HasPrefix(value, "v1/api/") || strings.HasPrefix(value, "/") {
114+
return routeKeyFromURI(value)
115+
}
116+
117+
if strings.TrimSpace(currentSource) == "" {
118+
return "", false
119+
}
120+
_, _, dqlRoot, ok := sourceRootsWithLayout(currentSource, layout)
121+
if !ok {
122+
return "", false
123+
}
124+
sourceNamespace, _ := dqlToRouteNamespaceWithLayout(currentSource, layout)
125+
namespace := resolveComponentNamespaceWithNamespace(value, currentSource, dqlRoot, sourceNamespace)
126+
if namespace == "" {
127+
return "", false
128+
}
129+
entries := r.ByNamespace[strings.ToLower(strings.TrimSpace(namespace))]
130+
if len(entries) == 0 {
131+
return "", false
132+
}
133+
if len(entries) == 1 {
134+
key := entries[0].RouteKey
135+
if _, conflicted := r.Conflicts[key]; conflicted {
136+
return "", false
137+
}
138+
return key, true
139+
}
140+
// Multiple methods under one namespace: require exact method match.
141+
for _, candidate := range entries {
142+
if candidate == nil {
143+
continue
144+
}
145+
if strings.EqualFold(candidate.Method, method) {
146+
if _, conflicted := r.Conflicts[candidate.RouteKey]; conflicted {
147+
return "", false
148+
}
149+
return candidate.RouteKey, true
150+
}
151+
}
152+
return "", false
153+
}
154+
155+
func (r *RouteIndex) addEntry(entry *RouteIndexEntry) {
156+
if r == nil || entry == nil {
157+
return
158+
}
159+
key := entry.RouteKey
160+
if prev, exists := r.ByRouteKey[key]; exists && prev != nil && prev.SourcePath != entry.SourcePath {
161+
if _, ok := r.Conflicts[key]; !ok {
162+
r.Conflicts[key] = []string{prev.SourcePath}
163+
}
164+
r.Conflicts[key] = append(r.Conflicts[key], entry.SourcePath)
165+
return
166+
}
167+
r.ByRouteKey[key] = entry
168+
nsKey := strings.ToLower(strings.TrimSpace(entry.Namespace))
169+
if nsKey != "" {
170+
r.ByNamespace[nsKey] = append(r.ByNamespace[nsKey], entry)
171+
}
172+
}
173+
174+
func parseRouteMethods(input string) []string {
175+
input = strings.TrimSpace(input)
176+
if input == "" {
177+
return []string{http.MethodGet}
178+
}
179+
parts := strings.Split(input, ",")
180+
ret := make([]string, 0, len(parts))
181+
seen := map[string]bool{}
182+
for _, part := range parts {
183+
method := strings.ToUpper(strings.TrimSpace(part))
184+
if method == "" {
185+
continue
186+
}
187+
if seen[method] {
188+
continue
189+
}
190+
seen[method] = true
191+
ret = append(ret, method)
192+
}
193+
if len(ret) == 0 {
194+
return []string{http.MethodGet}
195+
}
196+
return ret
197+
}
198+
199+
func normalizeRouteKey(method, uri string) string {
200+
method = strings.ToUpper(strings.TrimSpace(method))
201+
if method == "" {
202+
method = http.MethodGet
203+
}
204+
return method + ":" + normalizeURI(uri)
205+
}
206+
207+
func normalizeURI(uri string) string {
208+
uri = strings.TrimSpace(uri)
209+
if uri == "" {
210+
return "/"
211+
}
212+
if strings.HasPrefix(uri, "v1/api/") {
213+
uri = "/" + uri
214+
}
215+
if !strings.HasPrefix(uri, "/") {
216+
uri = "/" + uri
217+
}
218+
return uri
219+
}
220+
221+
func inferDefaultURI(namespace string) string {
222+
namespace = strings.Trim(strings.TrimSpace(namespace), "/")
223+
if namespace == "" {
224+
return ""
225+
}
226+
parts := strings.Split(namespace, "/")
227+
if len(parts) >= 2 && parts[len(parts)-1] == parts[len(parts)-2] {
228+
parts = parts[:len(parts)-1]
229+
}
230+
return "/v1/api/" + strings.Join(parts, "/")
231+
}

0 commit comments

Comments
 (0)