@cratis/scene.engine 4.8.0 → 4.10.0

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.
Files changed (40) hide show
  1. package/BindingDiagnostic.ts +11 -0
  2. package/BindingScope.ts +16 -0
  3. package/dist/cjs/index.js +7 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/resolveBindingExpression.js +51 -0
  6. package/dist/cjs/resolveBindingExpression.js.map +1 -0
  7. package/dist/cjs/validateBindingExpression.js +70 -0
  8. package/dist/cjs/validateBindingExpression.js.map +1 -0
  9. package/dist/cjs/validateNavigationDestinations.js +48 -0
  10. package/dist/cjs/validateNavigationDestinations.js.map +1 -0
  11. package/dist/esm/BindingDiagnostic.d.ts +6 -0
  12. package/dist/esm/BindingDiagnostic.d.ts.map +1 -0
  13. package/dist/esm/BindingDiagnostic.js +2 -0
  14. package/dist/esm/BindingDiagnostic.js.map +1 -0
  15. package/dist/esm/BindingScope.d.ts +6 -0
  16. package/dist/esm/BindingScope.d.ts.map +1 -0
  17. package/dist/esm/BindingScope.js +2 -0
  18. package/dist/esm/BindingScope.js.map +1 -0
  19. package/dist/esm/index.d.ts +5 -0
  20. package/dist/esm/index.d.ts.map +1 -1
  21. package/dist/esm/index.js +3 -0
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/resolveBindingExpression.d.ts +5 -0
  24. package/dist/esm/resolveBindingExpression.d.ts.map +1 -0
  25. package/dist/esm/resolveBindingExpression.js +48 -0
  26. package/dist/esm/resolveBindingExpression.js.map +1 -0
  27. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  28. package/dist/esm/validateBindingExpression.d.ts +8 -0
  29. package/dist/esm/validateBindingExpression.d.ts.map +1 -0
  30. package/dist/esm/validateBindingExpression.js +68 -0
  31. package/dist/esm/validateBindingExpression.js.map +1 -0
  32. package/dist/esm/validateNavigationDestinations.d.ts +10 -0
  33. package/dist/esm/validateNavigationDestinations.d.ts.map +1 -0
  34. package/dist/esm/validateNavigationDestinations.js +46 -0
  35. package/dist/esm/validateNavigationDestinations.js.map +1 -0
  36. package/index.ts +5 -0
  37. package/package.json +3 -3
  38. package/resolveBindingExpression.ts +55 -0
  39. package/validateBindingExpression.ts +78 -0
  40. package/validateNavigationDestinations.ts +43 -0
@@ -0,0 +1,8 @@
1
+ import { BindingExpression } from '@cratis/scene.model';
2
+ import { BindingDiagnostic } from './BindingDiagnostic';
3
+ import { BindingScope } from './BindingScope';
4
+ export declare function validateBindingExpression(binding: BindingExpression, scope: BindingScope, options?: {
5
+ targetElementId?: string;
6
+ resolvingElementIds?: string[];
7
+ }): BindingDiagnostic[];
8
+ //# sourceMappingURL=validateBindingExpression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateBindingExpression.d.ts","sourceRoot":"","sources":["../../validateBindingExpression.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAqD,MAAM,qBAAqB,CAAC;AAC3G,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAM9C,wBAAgB,yBAAyB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,YAAY,EACnB,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC3E,iBAAiB,EAAE,CAoCrB"}
@@ -0,0 +1,68 @@
1
+ import { BindingSourceKind, BindingMode, PropertyValueType } from '@cratis/scene.model';
2
+ import { resolveBindingExpression } from './resolveBindingExpression.js';
3
+
4
+ // Copyright (c) Cratis. All rights reserved.
5
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
6
+ /**
7
+ * Validates one binding against available runtime scope and optional component dependency stack.
8
+ */ function validateBindingExpression(binding, scope, options = {}) {
9
+ const diagnostics = [];
10
+ const kind = binding.kind ?? BindingSourceKind.DataContext;
11
+ if (kind === BindingSourceKind.QueryResult && !binding.query) {
12
+ diagnostics.push(problem('missingQuery', 'Query result bindings must name the query they read from.', 'query'));
13
+ }
14
+ if (kind === BindingSourceKind.ComponentProperty) {
15
+ if (!binding.componentId) {
16
+ diagnostics.push(problem('missingComponent', 'Component property bindings must name the source component id.', 'componentId'));
17
+ } else if (!scope.componentOutputs?.[binding.componentId]) {
18
+ diagnostics.push(problem('unknownComponent', `No component output scope exists for '${binding.componentId}'.`, 'componentId'));
19
+ }
20
+ if (binding.componentId && options.targetElementId === binding.componentId) {
21
+ diagnostics.push(problem('bindingCycle', `Component '${binding.componentId}' cannot bind to its own output.`, 'componentId'));
22
+ }
23
+ if (binding.componentId && options.resolvingElementIds?.includes(binding.componentId)) {
24
+ diagnostics.push(problem('bindingCycle', `Binding to '${binding.componentId}' would create a component binding cycle.`, 'componentId'));
25
+ }
26
+ }
27
+ if (binding.mode === BindingMode.TwoWay && kind !== BindingSourceKind.ComponentProperty) {
28
+ diagnostics.push(problem('unsupportedTwoWayBinding', 'Two-way bindings require a component output source.', 'mode'));
29
+ }
30
+ if (binding.expectedValueType !== undefined) {
31
+ const value = resolveBindingExpression(binding, scope);
32
+ if (!matchesExpectedType(value, binding.expectedValueType)) {
33
+ diagnostics.push(problem('bindingTypeMismatch', `Binding value does not match expected ${binding.expectedValueType}.`, 'expectedValueType'));
34
+ }
35
+ }
36
+ return diagnostics;
37
+ }
38
+ function problem(code, message, path) {
39
+ return {
40
+ code,
41
+ message,
42
+ path
43
+ };
44
+ }
45
+ function matchesExpectedType(value, type) {
46
+ if (value === null || value === undefined) return true;
47
+ switch(type){
48
+ case PropertyValueType.String:
49
+ case PropertyValueType.Enum:
50
+ case PropertyValueType.Icon:
51
+ case PropertyValueType.Destination:
52
+ case PropertyValueType.QueryReference:
53
+ return typeof value === 'string' || typeof value === 'object' && !Array.isArray(value);
54
+ case PropertyValueType.Number:
55
+ return typeof value === 'number' && !Number.isNaN(value);
56
+ case PropertyValueType.Boolean:
57
+ return typeof value === 'boolean';
58
+ case PropertyValueType.Collection:
59
+ return Array.isArray(value);
60
+ case PropertyValueType.Object:
61
+ return typeof value === 'object' && !Array.isArray(value);
62
+ case PropertyValueType.Json:
63
+ return true;
64
+ }
65
+ }
66
+
67
+ export { validateBindingExpression };
68
+ //# sourceMappingURL=validateBindingExpression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateBindingExpression.js","sources":["../../validateBindingExpression.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { BindingExpression, BindingMode, BindingSourceKind, PropertyValueType } from '@cratis/scene.model';\nimport { BindingDiagnostic } from './BindingDiagnostic';\nimport { BindingScope } from './BindingScope';\nimport { resolveBindingExpression } from './resolveBindingExpression';\n\n/**\n * Validates one binding against available runtime scope and optional component dependency stack.\n */\nexport function validateBindingExpression(\n binding: BindingExpression,\n scope: BindingScope,\n options: { targetElementId?: string; resolvingElementIds?: string[] } = {},\n): BindingDiagnostic[] {\n const diagnostics: BindingDiagnostic[] = [];\n const kind = binding.kind ?? BindingSourceKind.DataContext;\n\n if (kind === BindingSourceKind.QueryResult && !binding.query) {\n diagnostics.push(problem('missingQuery', 'Query result bindings must name the query they read from.', 'query'));\n }\n\n if (kind === BindingSourceKind.ComponentProperty) {\n if (!binding.componentId) {\n diagnostics.push(problem('missingComponent', 'Component property bindings must name the source component id.', 'componentId'));\n } else if (!scope.componentOutputs?.[binding.componentId]) {\n diagnostics.push(problem('unknownComponent', `No component output scope exists for '${binding.componentId}'.`, 'componentId'));\n }\n\n if (binding.componentId && options.targetElementId === binding.componentId) {\n diagnostics.push(problem('bindingCycle', `Component '${binding.componentId}' cannot bind to its own output.`, 'componentId'));\n }\n\n if (binding.componentId && options.resolvingElementIds?.includes(binding.componentId)) {\n diagnostics.push(problem('bindingCycle', `Binding to '${binding.componentId}' would create a component binding cycle.`, 'componentId'));\n }\n }\n\n if (binding.mode === BindingMode.TwoWay && kind !== BindingSourceKind.ComponentProperty) {\n diagnostics.push(problem('unsupportedTwoWayBinding', 'Two-way bindings require a component output source.', 'mode'));\n }\n\n if (binding.expectedValueType !== undefined) {\n const value = resolveBindingExpression(binding, scope);\n if (!matchesExpectedType(value, binding.expectedValueType)) {\n diagnostics.push(problem('bindingTypeMismatch', `Binding value does not match expected ${binding.expectedValueType}.`, 'expectedValueType'));\n }\n }\n\n return diagnostics;\n}\n\nfunction problem(code: string, message: string, path?: string): BindingDiagnostic {\n return { code, message, path };\n}\n\nfunction matchesExpectedType(value: unknown, type: PropertyValueType): boolean {\n if (value === null || value === undefined) return true;\n switch (type) {\n case PropertyValueType.String:\n case PropertyValueType.Enum:\n case PropertyValueType.Icon:\n case PropertyValueType.Destination:\n case PropertyValueType.QueryReference:\n return typeof value === 'string' || (typeof value === 'object' && !Array.isArray(value));\n case PropertyValueType.Number:\n return typeof value === 'number' && !Number.isNaN(value);\n case PropertyValueType.Boolean:\n return typeof value === 'boolean';\n case PropertyValueType.Collection:\n return Array.isArray(value);\n case PropertyValueType.Object:\n return typeof value === 'object' && !Array.isArray(value);\n case PropertyValueType.Json:\n return true;\n }\n}\n"],"names":["validateBindingExpression","binding","scope","options","diagnostics","kind","BindingSourceKind","DataContext","QueryResult","query","push","problem","ComponentProperty","componentId","componentOutputs","targetElementId","resolvingElementIds","includes","mode","BindingMode","TwoWay","expectedValueType","undefined","value","resolveBindingExpression","matchesExpectedType","code","message","path","type","PropertyValueType","String","Enum","Icon","Destination","QueryReference","Array","isArray","Number","isNaN","Boolean","Collection","Object","Json"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { DestinationReference, Layout, ScreenTemplate } from '@cratis/scene.model';
2
+ export interface NavigationDestinationDiagnostic {
3
+ code: string;
4
+ message: string;
5
+ }
6
+ export declare function validateNavigationDestinations(destinations: DestinationReference[], surfaces?: {
7
+ layouts?: Layout[];
8
+ screenTemplates?: ScreenTemplate[];
9
+ }): NavigationDestinationDiagnostic[];
10
+ //# sourceMappingURL=validateNavigationDestinations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateNavigationDestinations.d.ts","sourceRoot":"","sources":["../../validateNavigationDestinations.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEnF,MAAM,WAAW,+BAA+B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAKD,wBAAgB,8BAA8B,CAC1C,YAAY,EAAE,oBAAoB,EAAE,EACpC,QAAQ,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,cAAc,EAAE,CAAA;CAAO,GAC1E,+BAA+B,EAAE,CA0BnC"}
@@ -0,0 +1,46 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+ /**
4
+ * Validates destinations against the outlet/route declarations owned by layouts and templates.
5
+ */ function validateNavigationDestinations(destinations, surfaces = {}) {
6
+ const diagnostics = [];
7
+ const outlets = new Set();
8
+ const routes = new Set();
9
+ for (const surface of [
10
+ ...surfaces.layouts ?? [],
11
+ ...surfaces.screenTemplates ?? []
12
+ ]){
13
+ for (const outlet of surface.outlets ?? []){
14
+ if (outlets.has(outlet.name)) diagnostics.push({
15
+ code: 'duplicateOutlet',
16
+ message: `Outlet '${outlet.name}' is declared more than once.`
17
+ });
18
+ outlets.add(outlet.name);
19
+ }
20
+ }
21
+ for (const destination of destinations){
22
+ if (destination.outlet && !outlets.has(destination.outlet)) {
23
+ diagnostics.push({
24
+ code: 'missingOutlet',
25
+ message: `Destination targets missing outlet '${destination.outlet}'.`
26
+ });
27
+ }
28
+ if (destination.route) {
29
+ if (routes.has(destination.route)) diagnostics.push({
30
+ code: 'duplicateRoute',
31
+ message: `Route '${destination.route}' is used by more than one destination.`
32
+ });
33
+ routes.add(destination.route);
34
+ }
35
+ if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {
36
+ diagnostics.push({
37
+ code: 'unavailableTarget',
38
+ message: 'Destination must name a screen, stable identity, route or dialog.'
39
+ });
40
+ }
41
+ }
42
+ return diagnostics;
43
+ }
44
+
45
+ export { validateNavigationDestinations };
46
+ //# sourceMappingURL=validateNavigationDestinations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateNavigationDestinations.js","sources":["../../validateNavigationDestinations.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { DestinationReference, Layout, ScreenTemplate } from '@cratis/scene.model';\n\nexport interface NavigationDestinationDiagnostic {\n code: string;\n message: string;\n}\n\n/**\n * Validates destinations against the outlet/route declarations owned by layouts and templates.\n */\nexport function validateNavigationDestinations(\n destinations: DestinationReference[],\n surfaces: { layouts?: Layout[]; screenTemplates?: ScreenTemplate[] } = {},\n): NavigationDestinationDiagnostic[] {\n const diagnostics: NavigationDestinationDiagnostic[] = [];\n const outlets = new Set<string>();\n const routes = new Set<string>();\n\n for (const surface of [...(surfaces.layouts ?? []), ...(surfaces.screenTemplates ?? [])]) {\n for (const outlet of surface.outlets ?? []) {\n if (outlets.has(outlet.name)) diagnostics.push({ code: 'duplicateOutlet', message: `Outlet '${outlet.name}' is declared more than once.` });\n outlets.add(outlet.name);\n }\n }\n\n for (const destination of destinations) {\n if (destination.outlet && !outlets.has(destination.outlet)) {\n diagnostics.push({ code: 'missingOutlet', message: `Destination targets missing outlet '${destination.outlet}'.` });\n }\n if (destination.route) {\n if (routes.has(destination.route)) diagnostics.push({ code: 'duplicateRoute', message: `Route '${destination.route}' is used by more than one destination.` });\n routes.add(destination.route);\n }\n if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {\n diagnostics.push({ code: 'unavailableTarget', message: 'Destination must name a screen, stable identity, route or dialog.' });\n }\n }\n\n return diagnostics;\n}\n"],"names":["validateNavigationDestinations","destinations","surfaces","diagnostics","outlets","Set","routes","surface","layouts","screenTemplates","outlet","has","name","push","code","message","add","destination","route","screen","module","dialog"],"mappings":"AAAA;AACA;AASA;;AAEC,IACM,SAASA,8BAAAA,CACZC,YAAoC,EACpCC,QAAAA,GAAuE,EAAE,EAAA;AAEzE,IAAA,MAAMC,cAAiD,EAAE;AACzD,IAAA,MAAMC,UAAU,IAAIC,GAAAA,EAAAA;AACpB,IAAA,MAAMC,SAAS,IAAID,GAAAA,EAAAA;AAEnB,IAAA,KAAK,MAAME,OAAAA,IAAW;WAAKL,QAAAA,CAASM,OAAO,IAAI,EAAE;WAAON,QAAAA,CAASO,eAAe,IAAI;KAAI,CAAE;AACtF,QAAA,KAAK,MAAMC,MAAAA,IAAUH,OAAAA,CAAQH,OAAO,IAAI,EAAE,CAAE;YACxC,IAAIA,OAAAA,CAAQO,GAAG,CAACD,MAAAA,CAAOE,IAAI,CAAA,EAAGT,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,iBAAA;AAAmBC,gBAAAA,OAAAA,EAAS,CAAC,QAAQ,EAAEL,OAAOE,IAAI,CAAC,6BAA6B;AAAE,aAAA,CAAA;YACzIR,OAAAA,CAAQY,GAAG,CAACN,MAAAA,CAAOE,IAAI,CAAA;AAC3B,QAAA;AACJ,IAAA;IAEA,KAAK,MAAMK,eAAehB,YAAAA,CAAc;QACpC,IAAIgB,WAAAA,CAAYP,MAAM,IAAI,CAACN,QAAQO,GAAG,CAACM,WAAAA,CAAYP,MAAM,CAAA,EAAG;AACxDP,YAAAA,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,eAAA;AAAiBC,gBAAAA,OAAAA,EAAS,CAAC,oCAAoC,EAAEE,YAAYP,MAAM,CAAC,EAAE;AAAE,aAAA,CAAA;AACrH,QAAA;QACA,IAAIO,WAAAA,CAAYC,KAAK,EAAE;YACnB,IAAIZ,MAAAA,CAAOK,GAAG,CAACM,WAAAA,CAAYC,KAAK,CAAA,EAAGf,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,gBAAA;AAAkBC,gBAAAA,OAAAA,EAAS,CAAC,OAAO,EAAEE,YAAYC,KAAK,CAAC,uCAAuC;AAAE,aAAA,CAAA;YAC5JZ,MAAAA,CAAOU,GAAG,CAACC,WAAAA,CAAYC,KAAK,CAAA;AAChC,QAAA;AACA,QAAA,IAAI,CAACD,WAAAA,CAAYE,MAAM,IAAI,CAACF,WAAAA,CAAYG,MAAM,IAAI,CAACH,YAAYC,KAAK,IAAI,CAACD,WAAAA,CAAYI,MAAM,EAAE;AACzFlB,YAAAA,WAAAA,CAAYU,IAAI,CAAC;gBAAEC,IAAAA,EAAM,mBAAA;gBAAqBC,OAAAA,EAAS;AAAoE,aAAA,CAAA;AAC/H,QAAA;AACJ,IAAA;IAEA,OAAOZ,WAAAA;AACX;;"}
package/index.ts CHANGED
@@ -3,6 +3,10 @@
3
3
 
4
4
  export * from './Renderer';
5
5
  export * from './BindingResolver';
6
+ export * from './BindingScope';
7
+ export * from './BindingDiagnostic';
8
+ export * from './resolveBindingExpression';
9
+ export * from './validateBindingExpression';
6
10
  export * from './renderElement';
7
11
  export * from './elementKind';
8
12
  export * from './panelKind';
@@ -28,4 +32,5 @@ export * from './ActionDispatcher';
28
32
  export * from './runActions';
29
33
  export * from './resolveBehaviors';
30
34
  export * from './resolveStrings';
35
+ export * from './validateNavigationDestinations';
31
36
  export * from './editing';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.engine",
3
- "version": "4.8.0",
3
+ "version": "4.10.0",
4
4
  "description": "The Scene runtime: walks a Scene element tree and drives whichever renderer is plugged in. Does not know React, or any other rendering platform, exists.",
5
5
  "keywords": [
6
6
  "cratis",
@@ -48,9 +48,9 @@
48
48
  "up": "yarn g:up"
49
49
  },
50
50
  "dependencies": {
51
- "@cratis/scene.model": "4.8.0"
51
+ "@cratis/scene.model": "4.10.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cratis/scene.model": "4.8.0"
54
+ "@cratis/scene.model": "4.10.0"
55
55
  }
56
56
  }
@@ -0,0 +1,55 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { BindingExpression, BindingNullBehavior, BindingSourceKind } from '@cratis/scene.model';
5
+ import { BindingScope } from './BindingScope';
6
+
7
+ /**
8
+ * Resolves a typed binding against the shared runtime binding scope.
9
+ */
10
+ export function resolveBindingExpression(binding: BindingExpression, scope: BindingScope): unknown {
11
+ const kind = binding.kind ?? BindingSourceKind.DataContext;
12
+ if (kind === BindingSourceKind.Literal) return binding.value;
13
+
14
+ const source = sourceValue(binding, scope, kind);
15
+ const value = readPath(source, sourcePath(binding, kind));
16
+ if (value !== null && value !== undefined) return value;
17
+
18
+ if (binding.nullBehavior === BindingNullBehavior.Preserve) return undefined;
19
+ if (binding.nullBehavior === BindingNullBehavior.Clear) return null;
20
+ return value;
21
+ }
22
+
23
+ /**
24
+ * Creates a legacy resolver function from a binding scope.
25
+ */
26
+ export function createBindingResolver(scope: BindingScope): (binding: BindingExpression) => unknown {
27
+ return binding => resolveBindingExpression(binding, scope);
28
+ }
29
+
30
+ function sourceValue(binding: BindingExpression, scope: BindingScope, kind: BindingSourceKind): unknown {
31
+ switch (kind) {
32
+ case BindingSourceKind.DataContext:
33
+ return scope.dataContext;
34
+ case BindingSourceKind.QueryResult:
35
+ return binding.query ? scope.queryResults?.[binding.query] : undefined;
36
+ case BindingSourceKind.ComponentProperty:
37
+ return binding.componentId ? scope.componentOutputs?.[binding.componentId] : undefined;
38
+ case BindingSourceKind.Literal:
39
+ return binding.value;
40
+ }
41
+ }
42
+
43
+ function sourcePath(binding: BindingExpression, kind: BindingSourceKind): string {
44
+ if (kind === BindingSourceKind.ComponentProperty) return binding.componentPropertyPath ?? binding.path;
45
+ return binding.path;
46
+ }
47
+
48
+ function readPath(source: unknown, path: string): unknown {
49
+ if (!path) return source;
50
+ return path.split('.').reduce<unknown>((current, segment) => {
51
+ if (current === null || current === undefined) return undefined;
52
+ if (typeof current !== 'object') return undefined;
53
+ return (current as Record<string, unknown>)[segment];
54
+ }, source);
55
+ }
@@ -0,0 +1,78 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { BindingExpression, BindingMode, BindingSourceKind, PropertyValueType } from '@cratis/scene.model';
5
+ import { BindingDiagnostic } from './BindingDiagnostic';
6
+ import { BindingScope } from './BindingScope';
7
+ import { resolveBindingExpression } from './resolveBindingExpression';
8
+
9
+ /**
10
+ * Validates one binding against available runtime scope and optional component dependency stack.
11
+ */
12
+ export function validateBindingExpression(
13
+ binding: BindingExpression,
14
+ scope: BindingScope,
15
+ options: { targetElementId?: string; resolvingElementIds?: string[] } = {},
16
+ ): BindingDiagnostic[] {
17
+ const diagnostics: BindingDiagnostic[] = [];
18
+ const kind = binding.kind ?? BindingSourceKind.DataContext;
19
+
20
+ if (kind === BindingSourceKind.QueryResult && !binding.query) {
21
+ diagnostics.push(problem('missingQuery', 'Query result bindings must name the query they read from.', 'query'));
22
+ }
23
+
24
+ if (kind === BindingSourceKind.ComponentProperty) {
25
+ if (!binding.componentId) {
26
+ diagnostics.push(problem('missingComponent', 'Component property bindings must name the source component id.', 'componentId'));
27
+ } else if (!scope.componentOutputs?.[binding.componentId]) {
28
+ diagnostics.push(problem('unknownComponent', `No component output scope exists for '${binding.componentId}'.`, 'componentId'));
29
+ }
30
+
31
+ if (binding.componentId && options.targetElementId === binding.componentId) {
32
+ diagnostics.push(problem('bindingCycle', `Component '${binding.componentId}' cannot bind to its own output.`, 'componentId'));
33
+ }
34
+
35
+ if (binding.componentId && options.resolvingElementIds?.includes(binding.componentId)) {
36
+ diagnostics.push(problem('bindingCycle', `Binding to '${binding.componentId}' would create a component binding cycle.`, 'componentId'));
37
+ }
38
+ }
39
+
40
+ if (binding.mode === BindingMode.TwoWay && kind !== BindingSourceKind.ComponentProperty) {
41
+ diagnostics.push(problem('unsupportedTwoWayBinding', 'Two-way bindings require a component output source.', 'mode'));
42
+ }
43
+
44
+ if (binding.expectedValueType !== undefined) {
45
+ const value = resolveBindingExpression(binding, scope);
46
+ if (!matchesExpectedType(value, binding.expectedValueType)) {
47
+ diagnostics.push(problem('bindingTypeMismatch', `Binding value does not match expected ${binding.expectedValueType}.`, 'expectedValueType'));
48
+ }
49
+ }
50
+
51
+ return diagnostics;
52
+ }
53
+
54
+ function problem(code: string, message: string, path?: string): BindingDiagnostic {
55
+ return { code, message, path };
56
+ }
57
+
58
+ function matchesExpectedType(value: unknown, type: PropertyValueType): boolean {
59
+ if (value === null || value === undefined) return true;
60
+ switch (type) {
61
+ case PropertyValueType.String:
62
+ case PropertyValueType.Enum:
63
+ case PropertyValueType.Icon:
64
+ case PropertyValueType.Destination:
65
+ case PropertyValueType.QueryReference:
66
+ return typeof value === 'string' || (typeof value === 'object' && !Array.isArray(value));
67
+ case PropertyValueType.Number:
68
+ return typeof value === 'number' && !Number.isNaN(value);
69
+ case PropertyValueType.Boolean:
70
+ return typeof value === 'boolean';
71
+ case PropertyValueType.Collection:
72
+ return Array.isArray(value);
73
+ case PropertyValueType.Object:
74
+ return typeof value === 'object' && !Array.isArray(value);
75
+ case PropertyValueType.Json:
76
+ return true;
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DestinationReference, Layout, ScreenTemplate } from '@cratis/scene.model';
5
+
6
+ export interface NavigationDestinationDiagnostic {
7
+ code: string;
8
+ message: string;
9
+ }
10
+
11
+ /**
12
+ * Validates destinations against the outlet/route declarations owned by layouts and templates.
13
+ */
14
+ export function validateNavigationDestinations(
15
+ destinations: DestinationReference[],
16
+ surfaces: { layouts?: Layout[]; screenTemplates?: ScreenTemplate[] } = {},
17
+ ): NavigationDestinationDiagnostic[] {
18
+ const diagnostics: NavigationDestinationDiagnostic[] = [];
19
+ const outlets = new Set<string>();
20
+ const routes = new Set<string>();
21
+
22
+ for (const surface of [...(surfaces.layouts ?? []), ...(surfaces.screenTemplates ?? [])]) {
23
+ for (const outlet of surface.outlets ?? []) {
24
+ if (outlets.has(outlet.name)) diagnostics.push({ code: 'duplicateOutlet', message: `Outlet '${outlet.name}' is declared more than once.` });
25
+ outlets.add(outlet.name);
26
+ }
27
+ }
28
+
29
+ for (const destination of destinations) {
30
+ if (destination.outlet && !outlets.has(destination.outlet)) {
31
+ diagnostics.push({ code: 'missingOutlet', message: `Destination targets missing outlet '${destination.outlet}'.` });
32
+ }
33
+ if (destination.route) {
34
+ if (routes.has(destination.route)) diagnostics.push({ code: 'duplicateRoute', message: `Route '${destination.route}' is used by more than one destination.` });
35
+ routes.add(destination.route);
36
+ }
37
+ if (!destination.screen && !destination.module && !destination.route && !destination.dialog) {
38
+ diagnostics.push({ code: 'unavailableTarget', message: 'Destination must name a screen, stable identity, route or dialog.' });
39
+ }
40
+ }
41
+
42
+ return diagnostics;
43
+ }