@telorun/analyzer 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/dist/adapters/http-adapter.d.ts +1 -1
- package/dist/adapters/http-adapter.d.ts.map +1 -1
- package/dist/adapters/http-adapter.js +4 -3
- package/dist/adapters/node-adapter.d.ts +1 -1
- package/dist/adapters/node-adapter.d.ts.map +1 -1
- package/dist/adapters/node-adapter.js +2 -1
- package/dist/adapters/registry-adapter.d.ts +5 -1
- package/dist/adapters/registry-adapter.d.ts.map +1 -1
- package/dist/adapters/registry-adapter.js +27 -7
- package/dist/analysis-registry.d.ts +2 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +8 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +107 -4
- package/dist/cel-environment.d.ts +5 -2
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +5 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/kernel-globals.d.ts +34 -0
- package/dist/kernel-globals.d.ts.map +1 -0
- package/dist/kernel-globals.js +94 -0
- package/dist/manifest-loader.d.ts +6 -3
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +110 -5
- package/dist/schema-compat.d.ts +1 -1
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +82 -28
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/validate-cel-context.d.ts +26 -4
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +123 -15
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +13 -1
- package/package.json +21 -2
- package/src/adapters/http-adapter.ts +4 -4
- package/src/adapters/node-adapter.ts +2 -2
- package/src/adapters/registry-adapter.ts +30 -8
- package/src/analysis-registry.ts +10 -0
- package/src/analyzer.ts +139 -5
- package/src/cel-environment.ts +5 -3
- package/src/index.ts +2 -4
- package/src/kernel-globals.ts +110 -0
- package/src/manifest-loader.ts +131 -7
- package/src/schema-compat.ts +87 -31
- package/src/types.ts +14 -0
- package/src/validate-cel-context.ts +150 -15
- package/src/validate-references.ts +13 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kernel global names available in every CEL evaluation context at runtime.
|
|
5
|
+
* Both `buildKernelGlobalsSchema` (chain-access validation) and
|
|
6
|
+
* `buildTypedCelEnvironment` in cel-environment.ts (CEL type-checking)
|
|
7
|
+
* must stay in sync with this list.
|
|
8
|
+
*
|
|
9
|
+
* Note: `env` is only available in the root module context. Child modules
|
|
10
|
+
* loaded via Kernel.Import do not receive host environment variables.
|
|
11
|
+
* There is no `imports` namespace at runtime — import snapshots are stored
|
|
12
|
+
* under `resources.<alias>`.
|
|
13
|
+
*/
|
|
14
|
+
export const KERNEL_GLOBAL_NAMES = ["variables", "secrets", "resources", "env"] as const;
|
|
15
|
+
|
|
16
|
+
const SYSTEM_KINDS = new Set([
|
|
17
|
+
"Kernel.Definition",
|
|
18
|
+
"Kernel.Module",
|
|
19
|
+
"Kernel.Abstract",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a typed JSON Schema describing the kernel globals available in the
|
|
24
|
+
* given manifest set. Used to merge into `x-telo-context` schemas so that
|
|
25
|
+
* chain-access validation recognises kernel globals without module authors
|
|
26
|
+
* having to re-declare them.
|
|
27
|
+
*
|
|
28
|
+
* - `variables` / `secrets`: typed from the `Kernel.Module` declaration
|
|
29
|
+
* - `resources`: enumerates all non-system resource names
|
|
30
|
+
* - `env`: dynamic (runtime env vars, root module only)
|
|
31
|
+
*/
|
|
32
|
+
export function buildKernelGlobalsSchema(
|
|
33
|
+
manifests: ResourceManifest[],
|
|
34
|
+
): Record<string, any> {
|
|
35
|
+
const moduleManifest = manifests.find((m) => m.kind === "Kernel.Module") as
|
|
36
|
+
| Record<string, any>
|
|
37
|
+
| undefined;
|
|
38
|
+
|
|
39
|
+
const resourceProps: Record<string, any> = {};
|
|
40
|
+
for (const m of manifests) {
|
|
41
|
+
const name = m.metadata?.name as string | undefined;
|
|
42
|
+
if (!name || !m.kind) continue;
|
|
43
|
+
// Kernel.Import snapshots are stored under resources.<alias> at runtime,
|
|
44
|
+
// so they appear here alongside regular resources.
|
|
45
|
+
if (!SYSTEM_KINDS.has(m.kind)) {
|
|
46
|
+
resourceProps[name] = { type: "object", additionalProperties: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
variables: buildSchemaMapSchema(moduleManifest?.variables),
|
|
54
|
+
secrets: buildSchemaMapSchema(moduleManifest?.secrets),
|
|
55
|
+
resources: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: resourceProps,
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
env: { type: "object", additionalProperties: true },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Wrap a JSON Schema property map (like `Kernel.Module.variables`) into a
|
|
66
|
+
* closed object schema suitable for chain-access validation. Falls back to
|
|
67
|
+
* an open map when the module declares no variables/secrets. */
|
|
68
|
+
function buildSchemaMapSchema(
|
|
69
|
+
schemaMap: Record<string, any> | null | undefined,
|
|
70
|
+
): Record<string, any> {
|
|
71
|
+
if (!schemaMap || typeof schemaMap !== "object" || Array.isArray(schemaMap)) {
|
|
72
|
+
return { type: "object", additionalProperties: true };
|
|
73
|
+
}
|
|
74
|
+
const props: Record<string, any> = {};
|
|
75
|
+
for (const [key, value] of Object.entries(schemaMap)) {
|
|
76
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
77
|
+
props[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (Object.keys(props).length === 0) {
|
|
81
|
+
return { type: "object", additionalProperties: true };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: props,
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Merge kernel globals into an `x-telo-context` schema so chain-access
|
|
92
|
+
* validation recognises `variables`, `secrets`, `resources`, `env`
|
|
93
|
+
* without module authors having to re-declare them.
|
|
94
|
+
*
|
|
95
|
+
* Context-specific properties take precedence over globals (spread order).
|
|
96
|
+
* The original `additionalProperties` setting is preserved.
|
|
97
|
+
*/
|
|
98
|
+
export function mergeKernelGlobalsIntoContext(
|
|
99
|
+
contextSchema: Record<string, any>,
|
|
100
|
+
globalsSchema: Record<string, any>,
|
|
101
|
+
): Record<string, any> {
|
|
102
|
+
return {
|
|
103
|
+
...contextSchema,
|
|
104
|
+
properties: {
|
|
105
|
+
...globalsSchema.properties,
|
|
106
|
+
...(contextSchema.properties ?? {}),
|
|
107
|
+
},
|
|
108
|
+
additionalProperties: contextSchema.additionalProperties ?? false,
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/manifest-loader.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
|
|
3
3
|
import { HttpAdapter } from "./adapters/http-adapter.js";
|
|
4
4
|
import { RegistryAdapter } from "./adapters/registry-adapter.js";
|
|
5
5
|
import { precompileDoc } from "./precompile.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
LoadOptions,
|
|
8
|
+
LoaderInitOptions,
|
|
9
|
+
ManifestAdapter,
|
|
10
|
+
Position,
|
|
11
|
+
PositionIndex,
|
|
12
|
+
} from "./types.js";
|
|
7
13
|
|
|
8
14
|
export class Loader {
|
|
9
|
-
|
|
15
|
+
private static readonly moduleCache = new Map<
|
|
16
|
+
string,
|
|
17
|
+
{ text: string; manifests: ResourceManifest[] }
|
|
18
|
+
>();
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
protected adapters: ManifestAdapter[];
|
|
21
|
+
|
|
22
|
+
constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
|
|
23
|
+
const options: LoaderInitOptions = Array.isArray(extraAdaptersOrOptions)
|
|
24
|
+
? { extraAdapters: extraAdaptersOrOptions }
|
|
25
|
+
: extraAdaptersOrOptions;
|
|
26
|
+
|
|
27
|
+
const includeHttpAdapter = options.includeHttpAdapter ?? true;
|
|
28
|
+
const includeRegistryAdapter = options.includeRegistryAdapter ?? true;
|
|
29
|
+
|
|
30
|
+
this.adapters = [];
|
|
31
|
+
if (includeHttpAdapter) this.adapters.push(new HttpAdapter());
|
|
32
|
+
if (includeRegistryAdapter) this.adapters.push(new RegistryAdapter(options.registryUrl));
|
|
33
|
+
|
|
34
|
+
if (options.extraAdapters?.length) {
|
|
35
|
+
this.adapters.unshift(...options.extraAdapters);
|
|
36
|
+
}
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
register(adapter: ManifestAdapter): this {
|
|
@@ -23,8 +47,19 @@ export class Loader {
|
|
|
23
47
|
return a;
|
|
24
48
|
}
|
|
25
49
|
|
|
50
|
+
async resolveEntryPoint(url: string): Promise<string> {
|
|
51
|
+
const { source } = await this.pick(url).read(url);
|
|
52
|
+
return source;
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
async loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
|
|
27
56
|
const { text, source } = await this.pick(url).read(url);
|
|
57
|
+
const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
|
|
58
|
+
const cached = Loader.moduleCache.get(cacheKey);
|
|
59
|
+
if (cached && cached.text === text) {
|
|
60
|
+
return cloneManifestArray(cached.manifests);
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
const parsedDocuments = parseAllDocuments(text);
|
|
29
64
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
30
65
|
const offsets = documentLineOffsets(text);
|
|
@@ -79,12 +114,63 @@ export class Loader {
|
|
|
79
114
|
if (moduleName) {
|
|
80
115
|
for (const manifest of resolved) {
|
|
81
116
|
if (manifest.kind !== "Kernel.Module" && !manifest.metadata?.module) {
|
|
117
|
+
const pi = (manifest.metadata as any)?.positionIndex;
|
|
82
118
|
manifest.metadata = { ...manifest.metadata, module: moduleName };
|
|
119
|
+
if (pi) {
|
|
120
|
+
Object.defineProperty(manifest.metadata, "positionIndex", {
|
|
121
|
+
value: pi,
|
|
122
|
+
enumerable: false,
|
|
123
|
+
writable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
83
127
|
}
|
|
84
128
|
}
|
|
85
129
|
}
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
|
|
132
|
+
return cloneManifestArray(resolved);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async loadModuleGraph(
|
|
136
|
+
entryUrl: string,
|
|
137
|
+
onError?: (url: string, error: Error) => void,
|
|
138
|
+
): Promise<Map<string, ResourceManifest[]>> {
|
|
139
|
+
const visited = new Set<string>([entryUrl]);
|
|
140
|
+
const result = new Map<string, ResourceManifest[]>();
|
|
141
|
+
|
|
142
|
+
const entry = await this.loadModule(entryUrl);
|
|
143
|
+
result.set(entryUrl, entry);
|
|
144
|
+
|
|
145
|
+
const queue: ResourceManifest[] = [...entry];
|
|
146
|
+
|
|
147
|
+
while (queue.length > 0) {
|
|
148
|
+
const m = queue.shift()!;
|
|
149
|
+
if (m.kind !== "Kernel.Import") continue;
|
|
150
|
+
const importSource = (m as any).source as string | undefined;
|
|
151
|
+
if (!importSource) continue;
|
|
152
|
+
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
153
|
+
const importUrl =
|
|
154
|
+
importSource.startsWith(".") || importSource.startsWith("/")
|
|
155
|
+
? this.pick(base).resolveRelative(base, importSource)
|
|
156
|
+
: importSource;
|
|
157
|
+
if (visited.has(importUrl)) continue;
|
|
158
|
+
visited.add(importUrl);
|
|
159
|
+
let imported: ResourceManifest[];
|
|
160
|
+
try {
|
|
161
|
+
imported = await this.loadModule(importUrl);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
164
|
+
onError?.(importUrl, error);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
result.set(importUrl, imported);
|
|
168
|
+
for (const im of imported) {
|
|
169
|
+
if (im.kind === "Kernel.Import") queue.push(im);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
88
174
|
}
|
|
89
175
|
|
|
90
176
|
async loadManifests(entryUrl: string): Promise<ResourceManifest[]> {
|
|
@@ -100,7 +186,10 @@ export class Loader {
|
|
|
100
186
|
const importSource = (m as any).source as string | undefined;
|
|
101
187
|
if (!importSource) continue;
|
|
102
188
|
const base = (m.metadata as any)?.source ?? entryUrl;
|
|
103
|
-
const importUrl =
|
|
189
|
+
const importUrl =
|
|
190
|
+
importSource.startsWith(".") || importSource.startsWith("/")
|
|
191
|
+
? this.pick(base).resolveRelative(base, importSource)
|
|
192
|
+
: importSource;
|
|
104
193
|
if (visited.has(importUrl)) continue;
|
|
105
194
|
visited.add(importUrl);
|
|
106
195
|
let imported: ResourceManifest[];
|
|
@@ -113,11 +202,20 @@ export class Loader {
|
|
|
113
202
|
}
|
|
114
203
|
const importedModule = imported.find((im) => im.kind === "Kernel.Module");
|
|
115
204
|
if (importedModule?.metadata?.name) {
|
|
205
|
+
const pi = (m.metadata as any)?.positionIndex;
|
|
116
206
|
m.metadata = {
|
|
117
207
|
...m.metadata,
|
|
118
208
|
resolvedModuleName: importedModule.metadata.name as string,
|
|
119
209
|
resolvedNamespace: (importedModule.metadata as any).namespace ?? null,
|
|
120
210
|
};
|
|
211
|
+
if (pi) {
|
|
212
|
+
Object.defineProperty(m.metadata, "positionIndex", {
|
|
213
|
+
value: pi,
|
|
214
|
+
enumerable: false,
|
|
215
|
+
writable: true,
|
|
216
|
+
configurable: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
121
219
|
}
|
|
122
220
|
for (const im of imported) {
|
|
123
221
|
if (im.kind === "Kernel.Definition") importedDefs.push(im);
|
|
@@ -129,6 +227,32 @@ export class Loader {
|
|
|
129
227
|
}
|
|
130
228
|
}
|
|
131
229
|
|
|
230
|
+
function cloneManifestArray(manifests: ResourceManifest[]): ResourceManifest[] {
|
|
231
|
+
return manifests.map((manifest) => cloneManifestValue(manifest));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function cloneManifestValue<T>(value: T): T {
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return value.map((entry) => cloneManifestValue(entry)) as T;
|
|
237
|
+
}
|
|
238
|
+
if (isCompiledValue(value)) {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
if (value !== null && typeof value === "object") {
|
|
242
|
+
const source = value as Record<string, unknown>;
|
|
243
|
+
const clone: Record<string, unknown> = {};
|
|
244
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
245
|
+
clone[key] = cloneManifestValue(entry);
|
|
246
|
+
}
|
|
247
|
+
const positionIndex = Object.getOwnPropertyDescriptor(source, "positionIndex");
|
|
248
|
+
if (positionIndex) {
|
|
249
|
+
Object.defineProperty(clone, "positionIndex", positionIndex);
|
|
250
|
+
}
|
|
251
|
+
return clone as T;
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
132
256
|
function documentLineOffsets(text: string): number[] {
|
|
133
257
|
const offsets = [0];
|
|
134
258
|
const lines = text.split("\n");
|
package/src/schema-compat.ts
CHANGED
|
@@ -14,6 +14,7 @@ export function createAjv(): InstanceType<typeof Ajv> {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const ajv = createAjv();
|
|
17
|
+
const compiledSchemaValidators = new WeakMap<Record<string, any>, ReturnType<typeof ajv.compile>>();
|
|
17
18
|
|
|
18
19
|
export interface CompatibilityResult {
|
|
19
20
|
compatible: boolean;
|
|
@@ -81,7 +82,19 @@ function checkProperty(
|
|
|
81
82
|
|
|
82
83
|
export function formatSingleError(err: any): string {
|
|
83
84
|
const p = err.instancePath || "/";
|
|
84
|
-
|
|
85
|
+
const params = err.params ?? {};
|
|
86
|
+
switch (err.keyword) {
|
|
87
|
+
case "additionalProperties":
|
|
88
|
+
return `${p} must NOT have additional properties ('${params.additionalProperty}' is not allowed)`;
|
|
89
|
+
case "required":
|
|
90
|
+
return `${p} is missing required property '${params.missingProperty}'`;
|
|
91
|
+
case "enum":
|
|
92
|
+
return `${p} ${err.message ?? "is invalid"} (${(params.allowedValues as unknown[])?.join(" | ")})`;
|
|
93
|
+
case "type":
|
|
94
|
+
return `${p} must be ${params.type} (got ${typeof err.data})`;
|
|
95
|
+
default:
|
|
96
|
+
return `${p} ${err.message ?? "is invalid"}`;
|
|
97
|
+
}
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
export function formatAjvErrors(errors: any[] | null | undefined): string {
|
|
@@ -119,11 +132,14 @@ export interface SchemaIssue {
|
|
|
119
132
|
|
|
120
133
|
/** Validate actual data against a JSON Schema. Returns issues with path info, or empty array if valid. */
|
|
121
134
|
export function validateAgainstSchema(data: unknown, schema: Record<string, any>): SchemaIssue[] {
|
|
122
|
-
let validate
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
let validate = compiledSchemaValidators.get(schema);
|
|
136
|
+
if (!validate) {
|
|
137
|
+
try {
|
|
138
|
+
validate = ajv.compile(schema);
|
|
139
|
+
compiledSchemaValidators.set(schema, validate);
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
127
143
|
}
|
|
128
144
|
if (validate(data)) return [];
|
|
129
145
|
return (validate.errors ?? []).map((err: any) => ({
|
|
@@ -182,13 +198,20 @@ export function jsonSchemaToCelType(schema: Record<string, any> | undefined): st
|
|
|
182
198
|
if (schema.anyOf || schema.oneOf || schema.allOf) return "dyn";
|
|
183
199
|
if (Array.isArray(schema.type)) return "dyn";
|
|
184
200
|
switch (schema.type) {
|
|
185
|
-
case "integer":
|
|
186
|
-
|
|
187
|
-
case "
|
|
188
|
-
|
|
189
|
-
case "
|
|
190
|
-
|
|
191
|
-
case "
|
|
201
|
+
case "integer":
|
|
202
|
+
return "int";
|
|
203
|
+
case "number":
|
|
204
|
+
return "double";
|
|
205
|
+
case "string":
|
|
206
|
+
return "string";
|
|
207
|
+
case "boolean":
|
|
208
|
+
return "bool";
|
|
209
|
+
case "array":
|
|
210
|
+
return "list";
|
|
211
|
+
case "object":
|
|
212
|
+
return "map";
|
|
213
|
+
case "null":
|
|
214
|
+
return "null_type";
|
|
192
215
|
}
|
|
193
216
|
if (schema.properties) return "map";
|
|
194
217
|
if (schema.items) return "list";
|
|
@@ -196,10 +219,7 @@ export function jsonSchemaToCelType(schema: Record<string, any> | undefined): st
|
|
|
196
219
|
}
|
|
197
220
|
|
|
198
221
|
/** Check whether a CEL return type is compatible with a JSON Schema type constraint. */
|
|
199
|
-
export function celTypeSatisfiesJsonSchema(
|
|
200
|
-
celType: string,
|
|
201
|
-
schema: Record<string, any>,
|
|
202
|
-
): boolean {
|
|
222
|
+
export function celTypeSatisfiesJsonSchema(celType: string, schema: Record<string, any>): boolean {
|
|
203
223
|
if (celType === "dyn") return true;
|
|
204
224
|
if (!schema.type && !schema.anyOf && !schema.oneOf && !schema.allOf) return true;
|
|
205
225
|
if (schema.anyOf || schema.oneOf || schema.allOf) return true;
|
|
@@ -227,36 +247,72 @@ export function celPlaceholderForSchema(schema: Record<string, any>): unknown {
|
|
|
227
247
|
if (schema.default !== undefined) return schema.default;
|
|
228
248
|
switch (schema.type) {
|
|
229
249
|
case "integer":
|
|
230
|
-
case "number":
|
|
231
|
-
|
|
232
|
-
case "
|
|
233
|
-
|
|
234
|
-
case "
|
|
235
|
-
|
|
250
|
+
case "number":
|
|
251
|
+
return schema.minimum ?? 0;
|
|
252
|
+
case "string":
|
|
253
|
+
return "";
|
|
254
|
+
case "boolean":
|
|
255
|
+
return false;
|
|
256
|
+
case "array":
|
|
257
|
+
return [];
|
|
258
|
+
case "object":
|
|
259
|
+
return {};
|
|
260
|
+
default:
|
|
261
|
+
return null;
|
|
236
262
|
}
|
|
237
263
|
}
|
|
238
264
|
|
|
239
265
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
240
266
|
|
|
267
|
+
/** Resolve a `$ref` (only `#/$defs/...` form) against the root schema. */
|
|
268
|
+
function resolveRef(schema: Record<string, any>, root: Record<string, any>): Record<string, any> {
|
|
269
|
+
if (schema.$ref && typeof schema.$ref === "string" && schema.$ref.startsWith("#/$defs/")) {
|
|
270
|
+
const defName = schema.$ref.slice("#/$defs/".length);
|
|
271
|
+
const resolved = root.$defs?.[defName];
|
|
272
|
+
if (resolved) return resolved;
|
|
273
|
+
}
|
|
274
|
+
return schema;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Collect property schemas from top-level `properties` and all `oneOf`/`anyOf` sub-schemas. */
|
|
278
|
+
function collectProperties(schema: Record<string, any>): Record<string, any> {
|
|
279
|
+
const props: Record<string, any> = { ...(schema.properties ?? {}) };
|
|
280
|
+
for (const sub of schema.oneOf ?? schema.anyOf ?? []) {
|
|
281
|
+
if (sub && typeof sub === "object" && sub.properties) {
|
|
282
|
+
for (const [k, v] of Object.entries(sub.properties as Record<string, any>)) {
|
|
283
|
+
if (!(k in props)) props[k] = v;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return props;
|
|
288
|
+
}
|
|
289
|
+
|
|
241
290
|
/** Deep-clone `data`, replacing every pure CEL template string (`${{ expr }}`) with a
|
|
242
291
|
* schema-appropriate placeholder so AJV can validate non-CEL fields without false positives. */
|
|
243
|
-
export function substituteCelFields(
|
|
292
|
+
export function substituteCelFields(
|
|
293
|
+
data: unknown,
|
|
294
|
+
schema: Record<string, any>,
|
|
295
|
+
rootSchema?: Record<string, any>,
|
|
296
|
+
): unknown {
|
|
297
|
+
const root = rootSchema ?? schema;
|
|
298
|
+
const resolved = resolveRef(schema, root);
|
|
299
|
+
|
|
244
300
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
245
|
-
return celPlaceholderForSchema(
|
|
301
|
+
return celPlaceholderForSchema(resolved);
|
|
246
302
|
}
|
|
247
303
|
if (Array.isArray(data)) {
|
|
248
|
-
const itemSchema = (
|
|
249
|
-
return data.map((item) => substituteCelFields(item, itemSchema));
|
|
304
|
+
const itemSchema = resolveRef((resolved.items ?? {}) as Record<string, any>, root);
|
|
305
|
+
return data.map((item) => substituteCelFields(item, itemSchema, root));
|
|
250
306
|
}
|
|
251
307
|
if (data !== null && typeof data === "object") {
|
|
252
|
-
const props = (
|
|
308
|
+
const props = collectProperties(resolved);
|
|
253
309
|
const addlProps =
|
|
254
|
-
|
|
255
|
-
? (
|
|
310
|
+
resolved.additionalProperties && typeof resolved.additionalProperties === "object"
|
|
311
|
+
? (resolved.additionalProperties as Record<string, any>)
|
|
256
312
|
: undefined;
|
|
257
313
|
const result: Record<string, unknown> = {};
|
|
258
314
|
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
259
|
-
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}) as Record<string, any
|
|
315
|
+
result[k] = substituteCelFields(v, (props[k] ?? addlProps ?? {}) as Record<string, any>, root);
|
|
260
316
|
}
|
|
261
317
|
return result;
|
|
262
318
|
}
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,9 @@ export const DiagnosticSeverity = {
|
|
|
8
8
|
} as const;
|
|
9
9
|
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
10
10
|
|
|
11
|
+
/** Default entry-point filename when a directory is given instead of a file. */
|
|
12
|
+
export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
13
|
+
|
|
11
14
|
export interface Position {
|
|
12
15
|
/** 0-based line number */
|
|
13
16
|
line: number;
|
|
@@ -52,6 +55,17 @@ export interface LoadOptions {
|
|
|
52
55
|
compile?: boolean;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
export interface LoaderInitOptions {
|
|
59
|
+
/** Adapters inserted with highest priority before built-ins. */
|
|
60
|
+
extraAdapters?: ManifestAdapter[];
|
|
61
|
+
/** Include built-in HttpAdapter. Defaults to true. */
|
|
62
|
+
includeHttpAdapter?: boolean;
|
|
63
|
+
/** Include built-in RegistryAdapter. Defaults to true. */
|
|
64
|
+
includeRegistryAdapter?: boolean;
|
|
65
|
+
/** Base URL used by built-in RegistryAdapter when enabled. */
|
|
66
|
+
registryUrl?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
export interface AnalysisOptions {
|
|
56
70
|
strictContexts?: boolean;
|
|
57
71
|
}
|