capdag 0.184.465 → 0.187.479
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/RULES.md +8 -8
- package/build-browser.js +3 -3
- package/cap-fab-renderer.js +354 -45
- package/capdag.js +198 -188
- package/capdag.test.js +153 -149
- package/package.json +1 -1
package/capdag.js
CHANGED
|
@@ -174,7 +174,8 @@ function normalizeEffectValue(rawValue) {
|
|
|
174
174
|
|
|
175
175
|
function validateNonStructuralTags(tags) {
|
|
176
176
|
try {
|
|
177
|
-
new TaggedUrn('cap', tags
|
|
177
|
+
const serialized = new TaggedUrn('cap', tags).toString();
|
|
178
|
+
TaggedUrn.fromString(serialized);
|
|
178
179
|
} catch (error) {
|
|
179
180
|
const msg = error && error.message ? error.message : String(error);
|
|
180
181
|
const msgLower = msg.toLowerCase();
|
|
@@ -1038,6 +1039,8 @@ class CapUrnBuilder {
|
|
|
1038
1039
|
`Reserved structural key '${keyLower}' must be set via inSpec(), outSpec(), or effect()`
|
|
1039
1040
|
);
|
|
1040
1041
|
}
|
|
1042
|
+
const nextTags = { ...this._tags, [keyLower]: value };
|
|
1043
|
+
validateNonStructuralTags(nextTags);
|
|
1041
1044
|
this._tags[keyLower] = value;
|
|
1042
1045
|
return this;
|
|
1043
1046
|
}
|
|
@@ -1058,6 +1061,8 @@ class CapUrnBuilder {
|
|
|
1058
1061
|
`Reserved structural key '${keyLower}' cannot be used as a marker`
|
|
1059
1062
|
);
|
|
1060
1063
|
}
|
|
1064
|
+
const nextTags = { ...this._tags, [keyLower]: '*' };
|
|
1065
|
+
validateNonStructuralTags(nextTags);
|
|
1061
1066
|
this._tags[keyLower] = '*';
|
|
1062
1067
|
return this;
|
|
1063
1068
|
}
|
|
@@ -1143,21 +1148,21 @@ class CapMatcher {
|
|
|
1143
1148
|
}
|
|
1144
1149
|
|
|
1145
1150
|
// ============================================================================
|
|
1146
|
-
// MEDIA
|
|
1151
|
+
// MEDIA DEFINITION PARSING
|
|
1147
1152
|
// ============================================================================
|
|
1148
1153
|
|
|
1149
1154
|
/**
|
|
1150
|
-
*
|
|
1155
|
+
* MediaDef error types
|
|
1151
1156
|
*/
|
|
1152
|
-
class
|
|
1157
|
+
class MediaDefError extends Error {
|
|
1153
1158
|
constructor(code, message) {
|
|
1154
1159
|
super(message);
|
|
1155
|
-
this.name = '
|
|
1160
|
+
this.name = 'MediaDefError';
|
|
1156
1161
|
this.code = code;
|
|
1157
1162
|
}
|
|
1158
1163
|
}
|
|
1159
1164
|
|
|
1160
|
-
const
|
|
1165
|
+
const MediaDefErrorCodes = {
|
|
1161
1166
|
UNRESOLVABLE_MEDIA_URN: 1,
|
|
1162
1167
|
DUPLICATE_MEDIA_URN: 2
|
|
1163
1168
|
};
|
|
@@ -1168,7 +1173,7 @@ const MediaSpecErrorCodes = {
|
|
|
1168
1173
|
|
|
1169
1174
|
/**
|
|
1170
1175
|
* Well-known built-in media URN constants
|
|
1171
|
-
* These media URNs are implicitly available and do not need to be declared in
|
|
1176
|
+
* These media URNs are implicitly available and do not need to be declared in mediaDefs
|
|
1172
1177
|
*
|
|
1173
1178
|
* Cardinality and Structure use orthogonal marker tags:
|
|
1174
1179
|
* - `list` marker: presence = list/array, absence = scalar (default)
|
|
@@ -1361,11 +1366,11 @@ const MEDIA_COLLECTION_LIST = 'media:collection;list;record';
|
|
|
1361
1366
|
// Media URN for adapter selection output - JSON record
|
|
1362
1367
|
const MEDIA_ADAPTER_SELECTION = 'media:adapter-selection;json;record';
|
|
1363
1368
|
// Fabric registry lookup wire types (consumed/produced by cap:lookup-cap;fabric
|
|
1364
|
-
// and cap:lookup-media-
|
|
1369
|
+
// and cap:lookup-media-def;fabric, both implemented by fetchcartridge).
|
|
1365
1370
|
const MEDIA_CAP_URN = 'media:cap-urn;textable';
|
|
1366
1371
|
const MEDIA_MEDIA_URN = 'media:media-urn;textable';
|
|
1367
1372
|
const MEDIA_CAP_DEFINITION = 'media:cap-definition;json;record;textable';
|
|
1368
|
-
const
|
|
1373
|
+
const MEDIA_MEDIA_DEFINITION = 'media:media-definition;json;record;textable';
|
|
1369
1374
|
|
|
1370
1375
|
// =============================================================================
|
|
1371
1376
|
// STANDARD CAP URN CONSTANTS
|
|
@@ -1381,9 +1386,9 @@ const CAP_ADAPTER_SELECTION = 'cap:in="media:";out="media:adapter-selection;json
|
|
|
1381
1386
|
|
|
1382
1387
|
// Fabric registry lookup caps. Implemented by fetchcartridge.
|
|
1383
1388
|
// CAP_LOOKUP_CAP_FABRIC resolves a canonical cap URN to its full flattened
|
|
1384
|
-
// cap definition;
|
|
1389
|
+
// cap definition; CAP_LOOKUP_MEDIA_DEF_FABRIC does the same for media defs.
|
|
1385
1390
|
const CAP_LOOKUP_CAP_FABRIC = 'cap:in="media:cap-urn;textable";fabric;lookup-cap;out="media:cap-definition;json;record;textable"';
|
|
1386
|
-
const
|
|
1391
|
+
const CAP_LOOKUP_MEDIA_DEF_FABRIC = 'cap:in="media:media-urn;textable";fabric;lookup-media-def;out="media:media-definition;json;record;textable"';
|
|
1387
1392
|
|
|
1388
1393
|
// =============================================================================
|
|
1389
1394
|
// MEDIA URN CLASS
|
|
@@ -1786,21 +1791,21 @@ function getProfileURL(profileName) {
|
|
|
1786
1791
|
// MEDIA URN TAG UTILITIES
|
|
1787
1792
|
// =============================================================================
|
|
1788
1793
|
// NOTE: The MEDIA_X constants above are convenience values for referencing
|
|
1789
|
-
// common media URNs in code. Resolution must go through
|
|
1794
|
+
// common media URNs in code. Resolution must go through mediaDefs tables -
|
|
1790
1795
|
// there is NO built-in resolution.
|
|
1791
1796
|
|
|
1792
1797
|
/**
|
|
1793
|
-
* Resolved
|
|
1798
|
+
* Resolved MediaDef structure
|
|
1794
1799
|
*
|
|
1795
|
-
* A
|
|
1796
|
-
* a value type in the CAPDAG system.
|
|
1800
|
+
* A MediaDef is a resolved media definition containing information about
|
|
1801
|
+
* a value type in the CAPDAG system. MediaDefs are identified by unique media URNs
|
|
1797
1802
|
* and contain fields like media_type, profile_uri, schema, etc.
|
|
1798
1803
|
*
|
|
1799
|
-
*
|
|
1804
|
+
* MediaDefs are defined in JSON files in the registry or inline in cap definitions.
|
|
1800
1805
|
*/
|
|
1801
|
-
class
|
|
1806
|
+
class MediaDef {
|
|
1802
1807
|
/**
|
|
1803
|
-
* Create a new
|
|
1808
|
+
* Create a new MediaDef
|
|
1804
1809
|
* @param {string} contentType - The MIME content type
|
|
1805
1810
|
* @param {string|null} profile - Optional profile URL
|
|
1806
1811
|
* @param {Object|null} schema - Optional JSON Schema for local validation
|
|
@@ -1810,7 +1815,7 @@ class MediaSpec {
|
|
|
1810
1815
|
* @param {Object|null} validation - Optional validation rules (min, max, min_length, max_length, pattern, allowed_values)
|
|
1811
1816
|
* @param {Object|null} metadata - Optional metadata (arbitrary key-value pairs for display/categorization)
|
|
1812
1817
|
* @param {string[]} extensions - File extensions for storing this media type (e.g., ['pdf'], ['jpg', 'jpeg'])
|
|
1813
|
-
* @param {string|null} documentation - Optional long-form markdown documentation. Rendered in media info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the media
|
|
1818
|
+
* @param {string|null} documentation - Optional long-form markdown documentation. Rendered in media info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the media def is useful.
|
|
1814
1819
|
*/
|
|
1815
1820
|
constructor(contentType, profile = null, schema = null, title = null, description = null, mediaUrn = null, validation = null, metadata = null, extensions = [], documentation = null) {
|
|
1816
1821
|
this.contentType = contentType;
|
|
@@ -1929,7 +1934,7 @@ class MediaSpec {
|
|
|
1929
1934
|
/**
|
|
1930
1935
|
* Get the canonical string representation
|
|
1931
1936
|
* Format: <media-type>; profile="<url>" (no content-type: prefix)
|
|
1932
|
-
* @returns {string} The
|
|
1937
|
+
* @returns {string} The media_def as a string
|
|
1933
1938
|
*/
|
|
1934
1939
|
toString() {
|
|
1935
1940
|
if (this.profile) {
|
|
@@ -1939,37 +1944,37 @@ class MediaSpec {
|
|
|
1939
1944
|
}
|
|
1940
1945
|
|
|
1941
1946
|
/**
|
|
1942
|
-
* Get
|
|
1947
|
+
* Get MediaDef from a CapUrn using the output media URN
|
|
1943
1948
|
* NOTE: outSpec is now a required first-class field on CapUrn
|
|
1944
1949
|
* @param {CapUrn} capUrn - The cap URN
|
|
1945
|
-
* @param {Object}
|
|
1946
|
-
* @returns {
|
|
1947
|
-
* @throws {
|
|
1950
|
+
* @param {Object} mediaDefs - Optional mediaDefs lookup table for resolution
|
|
1951
|
+
* @returns {MediaDef} The resolved MediaDef
|
|
1952
|
+
* @throws {MediaDefError} If media URN cannot be resolved
|
|
1948
1953
|
*/
|
|
1949
|
-
static fromCapUrn(capUrn,
|
|
1954
|
+
static fromCapUrn(capUrn, mediaDefs = []) {
|
|
1950
1955
|
// outSpec is now a required field, so it's always present
|
|
1951
1956
|
const mediaUrn = capUrn.getOutSpec();
|
|
1952
1957
|
|
|
1953
|
-
// Resolve the media URN to a
|
|
1954
|
-
return resolveMediaUrn(mediaUrn,
|
|
1958
|
+
// Resolve the media URN to a MediaDef - no fallbacks, fail hard
|
|
1959
|
+
return resolveMediaUrn(mediaUrn, mediaDefs);
|
|
1955
1960
|
}
|
|
1956
1961
|
}
|
|
1957
1962
|
|
|
1958
1963
|
/**
|
|
1959
|
-
* Resolve a media URN to a
|
|
1964
|
+
* Resolve a media URN to a MediaDef
|
|
1960
1965
|
*
|
|
1961
|
-
* Resolution: Look up mediaUrn in
|
|
1962
|
-
* There is no built-in resolution - all media URNs must be in
|
|
1966
|
+
* Resolution: Look up mediaUrn in mediaDefs array (by urn field), FAIL HARD if not found.
|
|
1967
|
+
* There is no built-in resolution - all media URNs must be in mediaDefs.
|
|
1963
1968
|
*
|
|
1964
1969
|
* @param {string} mediaUrn - The media URN (e.g., "media:textable")
|
|
1965
|
-
* @param {Array}
|
|
1966
|
-
* @returns {
|
|
1967
|
-
* @throws {
|
|
1970
|
+
* @param {Array} mediaDefs - The mediaDefs array (each item has urn, media_type, title, etc.)
|
|
1971
|
+
* @returns {MediaDef} The resolved MediaDef
|
|
1972
|
+
* @throws {MediaDefError} If media URN cannot be resolved
|
|
1968
1973
|
*/
|
|
1969
|
-
function resolveMediaUrn(mediaUrn,
|
|
1970
|
-
// Look up in
|
|
1971
|
-
if (
|
|
1972
|
-
const def =
|
|
1974
|
+
function resolveMediaUrn(mediaUrn, mediaDefs = []) {
|
|
1975
|
+
// Look up in mediaDefs array by urn field
|
|
1976
|
+
if (mediaDefs && Array.isArray(mediaDefs)) {
|
|
1977
|
+
const def = mediaDefs.find(spec => spec.urn === mediaUrn);
|
|
1973
1978
|
|
|
1974
1979
|
if (def) {
|
|
1975
1980
|
// Object form: { urn, media_type, title, profile_uri?, schema?, description?, documentation?, validation?, metadata?, extensions? }
|
|
@@ -1990,37 +1995,37 @@ function resolveMediaUrn(mediaUrn, mediaSpecs = []) {
|
|
|
1990
1995
|
const extensions = Array.isArray(def.extensions) ? def.extensions : [];
|
|
1991
1996
|
|
|
1992
1997
|
if (!mediaType) {
|
|
1993
|
-
throw new
|
|
1994
|
-
|
|
1998
|
+
throw new MediaDefError(
|
|
1999
|
+
MediaDefErrorCodes.UNRESOLVABLE_MEDIA_URN,
|
|
1995
2000
|
`Media URN '${mediaUrn}' has invalid definition: missing media_type`
|
|
1996
2001
|
);
|
|
1997
2002
|
}
|
|
1998
2003
|
|
|
1999
|
-
return new
|
|
2004
|
+
return new MediaDef(mediaType, profileUri, schema, title, description, mediaUrn, validation, metadata, extensions, documentation);
|
|
2000
2005
|
}
|
|
2001
2006
|
}
|
|
2002
2007
|
|
|
2003
|
-
// FAIL HARD - media URN must be in
|
|
2004
|
-
throw new
|
|
2005
|
-
|
|
2006
|
-
`Cannot resolve media URN: '${mediaUrn}'. Not found in
|
|
2008
|
+
// FAIL HARD - media URN must be in mediaDefs array
|
|
2009
|
+
throw new MediaDefError(
|
|
2010
|
+
MediaDefErrorCodes.UNRESOLVABLE_MEDIA_URN,
|
|
2011
|
+
`Cannot resolve media URN: '${mediaUrn}'. Not found in mediaDefs array.`
|
|
2007
2012
|
);
|
|
2008
2013
|
}
|
|
2009
2014
|
|
|
2010
2015
|
/**
|
|
2011
|
-
* Build an extension index from a
|
|
2016
|
+
* Build an extension index from a mediaDefs array.
|
|
2012
2017
|
* Maps lowercase extension strings to arrays of media URNs that use that extension.
|
|
2013
2018
|
*
|
|
2014
|
-
* @param {Array}
|
|
2019
|
+
* @param {Array} mediaDefs - The mediaDefs array
|
|
2015
2020
|
* @returns {Map<string, string[]>} Map from extension to list of URNs
|
|
2016
2021
|
*/
|
|
2017
|
-
function buildExtensionIndex(
|
|
2022
|
+
function buildExtensionIndex(mediaDefs) {
|
|
2018
2023
|
const index = new Map();
|
|
2019
|
-
if (!
|
|
2024
|
+
if (!mediaDefs || !Array.isArray(mediaDefs)) {
|
|
2020
2025
|
return index;
|
|
2021
2026
|
}
|
|
2022
2027
|
|
|
2023
|
-
for (const spec of
|
|
2028
|
+
for (const spec of mediaDefs) {
|
|
2024
2029
|
if (!spec.urn || !Array.isArray(spec.extensions)) continue;
|
|
2025
2030
|
for (const ext of spec.extensions) {
|
|
2026
2031
|
const extLower = ext.toLowerCase();
|
|
@@ -2046,24 +2051,24 @@ function buildExtensionIndex(mediaSpecs) {
|
|
|
2046
2051
|
* Lookup is case-insensitive.
|
|
2047
2052
|
*
|
|
2048
2053
|
* @param {string} extension - The file extension to look up (without leading dot)
|
|
2049
|
-
* @param {Array}
|
|
2054
|
+
* @param {Array} mediaDefs - The mediaDefs array
|
|
2050
2055
|
* @returns {string[]} Array of media URNs for the extension
|
|
2051
|
-
* @throws {
|
|
2056
|
+
* @throws {MediaDefError} If no media def is registered for the given extension
|
|
2052
2057
|
*
|
|
2053
2058
|
* @example
|
|
2054
|
-
* const urns = mediaUrnsForExtension('pdf',
|
|
2059
|
+
* const urns = mediaUrnsForExtension('pdf', mediaDefs);
|
|
2055
2060
|
* // May return ['media:pdf']
|
|
2056
2061
|
*/
|
|
2057
|
-
function mediaUrnsForExtension(extension,
|
|
2058
|
-
const index = buildExtensionIndex(
|
|
2062
|
+
function mediaUrnsForExtension(extension, mediaDefs) {
|
|
2063
|
+
const index = buildExtensionIndex(mediaDefs);
|
|
2059
2064
|
const extLower = extension.toLowerCase();
|
|
2060
2065
|
const urns = index.get(extLower);
|
|
2061
2066
|
|
|
2062
2067
|
if (!urns || urns.length === 0) {
|
|
2063
|
-
throw new
|
|
2064
|
-
|
|
2065
|
-
`No media
|
|
2066
|
-
`Ensure the media
|
|
2068
|
+
throw new MediaDefError(
|
|
2069
|
+
MediaDefErrorCodes.UNRESOLVABLE_MEDIA_URN,
|
|
2070
|
+
`No media def registered for extension '${extension}'. ` +
|
|
2071
|
+
`Ensure the media def is defined with an 'extensions' array containing '${extension}'.`
|
|
2067
2072
|
);
|
|
2068
2073
|
}
|
|
2069
2074
|
|
|
@@ -2075,29 +2080,29 @@ function mediaUrnsForExtension(extension, mediaSpecs) {
|
|
|
2075
2080
|
*
|
|
2076
2081
|
* Returns an array of [extension, urns] pairs for debugging and introspection.
|
|
2077
2082
|
*
|
|
2078
|
-
* @param {Array}
|
|
2083
|
+
* @param {Array} mediaDefs - The mediaDefs array
|
|
2079
2084
|
* @returns {Array<[string, string[]]>} Array of [extension, urns] pairs
|
|
2080
2085
|
*/
|
|
2081
|
-
function getExtensionMappings(
|
|
2082
|
-
const index = buildExtensionIndex(
|
|
2086
|
+
function getExtensionMappings(mediaDefs) {
|
|
2087
|
+
const index = buildExtensionIndex(mediaDefs);
|
|
2083
2088
|
return Array.from(index.entries());
|
|
2084
2089
|
}
|
|
2085
2090
|
|
|
2086
2091
|
/**
|
|
2087
|
-
* Validate that
|
|
2092
|
+
* Validate that media_defs array has no duplicate URNs.
|
|
2088
2093
|
*
|
|
2089
|
-
* @param {Array}
|
|
2094
|
+
* @param {Array} mediaDefs - The mediaDefs array to validate
|
|
2090
2095
|
* @returns {{valid: boolean, error?: string, duplicates?: string[]}}
|
|
2091
2096
|
*/
|
|
2092
|
-
function
|
|
2093
|
-
if (!
|
|
2097
|
+
function validateNoMediaDefDuplicates(mediaDefs) {
|
|
2098
|
+
if (!mediaDefs || !Array.isArray(mediaDefs) || mediaDefs.length === 0) {
|
|
2094
2099
|
return { valid: true };
|
|
2095
2100
|
}
|
|
2096
2101
|
|
|
2097
2102
|
const seen = new Set();
|
|
2098
2103
|
const duplicates = [];
|
|
2099
2104
|
|
|
2100
|
-
for (const spec of
|
|
2105
|
+
for (const spec of mediaDefs) {
|
|
2101
2106
|
if (!spec.urn) continue;
|
|
2102
2107
|
if (seen.has(spec.urn)) {
|
|
2103
2108
|
duplicates.push(spec.urn);
|
|
@@ -2109,7 +2114,7 @@ function validateNoMediaSpecDuplicates(mediaSpecs) {
|
|
|
2109
2114
|
if (duplicates.length > 0) {
|
|
2110
2115
|
return {
|
|
2111
2116
|
valid: false,
|
|
2112
|
-
error: `Duplicate media URNs in
|
|
2117
|
+
error: `Duplicate media URNs in media_defs: ${duplicates.join(', ')}`,
|
|
2113
2118
|
duplicates
|
|
2114
2119
|
};
|
|
2115
2120
|
}
|
|
@@ -2118,20 +2123,20 @@ function validateNoMediaSpecDuplicates(mediaSpecs) {
|
|
|
2118
2123
|
}
|
|
2119
2124
|
|
|
2120
2125
|
/**
|
|
2121
|
-
* XV5: Validate that inline
|
|
2126
|
+
* XV5: Validate that inline media_defs don't redefine existing registry specs.
|
|
2122
2127
|
*
|
|
2123
2128
|
* Validation requires a registryLookup function to check if media URNs exist.
|
|
2124
2129
|
* If no registryLookup is provided, validation passes (graceful degradation).
|
|
2125
2130
|
*
|
|
2126
|
-
* @param {Array}
|
|
2131
|
+
* @param {Array} mediaDefs - The inline media_defs array from a capability
|
|
2127
2132
|
* @param {Object} [options] - Validation options
|
|
2128
2133
|
* @param {Function} [options.registryLookup] - Function to check if media URN exists in registry
|
|
2129
2134
|
* Returns true if exists, false otherwise
|
|
2130
2135
|
* Should handle errors gracefully (return false)
|
|
2131
2136
|
* @returns {Promise<{valid: boolean, error?: string, redefines?: string[]}>}
|
|
2132
2137
|
*/
|
|
2133
|
-
async function
|
|
2134
|
-
if (!
|
|
2138
|
+
async function validateNoMediaDefRedefinition(mediaDefs, options = {}) {
|
|
2139
|
+
if (!mediaDefs || !Array.isArray(mediaDefs) || mediaDefs.length === 0) {
|
|
2135
2140
|
return { valid: true };
|
|
2136
2141
|
}
|
|
2137
2142
|
|
|
@@ -2144,7 +2149,7 @@ async function validateNoMediaSpecRedefinition(mediaSpecs, options = {}) {
|
|
|
2144
2149
|
|
|
2145
2150
|
const redefines = [];
|
|
2146
2151
|
|
|
2147
|
-
for (const spec of
|
|
2152
|
+
for (const spec of mediaDefs) {
|
|
2148
2153
|
const mediaUrn = spec.urn;
|
|
2149
2154
|
if (!mediaUrn) continue;
|
|
2150
2155
|
try {
|
|
@@ -2161,7 +2166,7 @@ async function validateNoMediaSpecRedefinition(mediaSpecs, options = {}) {
|
|
|
2161
2166
|
if (redefines.length > 0) {
|
|
2162
2167
|
return {
|
|
2163
2168
|
valid: false,
|
|
2164
|
-
error: `XV5: Inline media
|
|
2169
|
+
error: `XV5: Inline media defs redefine existing registry specs: ${redefines.join(', ')}`,
|
|
2165
2170
|
redefines
|
|
2166
2171
|
};
|
|
2167
2172
|
}
|
|
@@ -2173,13 +2178,13 @@ async function validateNoMediaSpecRedefinition(mediaSpecs, options = {}) {
|
|
|
2173
2178
|
* XV5: Synchronous version that checks against a provided lookup function.
|
|
2174
2179
|
* If no registryLookup is provided, validation passes (graceful degradation).
|
|
2175
2180
|
*
|
|
2176
|
-
* @param {Array}
|
|
2181
|
+
* @param {Array} mediaDefs - The inline media_defs array from a capability
|
|
2177
2182
|
* @param {Function} [registryLookup] - Synchronous function to check if media URN exists
|
|
2178
2183
|
* Returns true if exists, false otherwise
|
|
2179
2184
|
* @returns {{valid: boolean, error?: string, redefines?: string[]}}
|
|
2180
2185
|
*/
|
|
2181
|
-
function
|
|
2182
|
-
if (!
|
|
2186
|
+
function validateNoMediaDefRedefinitionSync(mediaDefs, registryLookup = null) {
|
|
2187
|
+
if (!mediaDefs || !Array.isArray(mediaDefs) || mediaDefs.length === 0) {
|
|
2183
2188
|
return { valid: true };
|
|
2184
2189
|
}
|
|
2185
2190
|
|
|
@@ -2190,7 +2195,7 @@ function validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup = null)
|
|
|
2190
2195
|
|
|
2191
2196
|
const redefines = [];
|
|
2192
2197
|
|
|
2193
|
-
for (const spec of
|
|
2198
|
+
for (const spec of mediaDefs) {
|
|
2194
2199
|
const mediaUrn = spec.urn;
|
|
2195
2200
|
if (!mediaUrn) continue;
|
|
2196
2201
|
if (registryLookup(mediaUrn)) {
|
|
@@ -2201,7 +2206,7 @@ function validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup = null)
|
|
|
2201
2206
|
if (redefines.length > 0) {
|
|
2202
2207
|
return {
|
|
2203
2208
|
valid: false,
|
|
2204
|
-
error: `XV5: Inline media
|
|
2209
|
+
error: `XV5: Inline media defs redefine existing registry specs: ${redefines.join(', ')}`,
|
|
2205
2210
|
redefines
|
|
2206
2211
|
};
|
|
2207
2212
|
}
|
|
@@ -2213,13 +2218,13 @@ function validateNoMediaSpecRedefinitionSync(mediaSpecs, registryLookup = null)
|
|
|
2213
2218
|
* Check if a CapUrn represents binary output.
|
|
2214
2219
|
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
2215
2220
|
* @param {CapUrn} capUrn - The cap URN
|
|
2216
|
-
* @param {Array}
|
|
2221
|
+
* @param {Array} mediaDefs - Optional mediaDefs array
|
|
2217
2222
|
* @returns {boolean} True if binary
|
|
2218
|
-
* @throws {
|
|
2223
|
+
* @throws {MediaDefError} If 'out' tag is missing or spec ID cannot be resolved
|
|
2219
2224
|
*/
|
|
2220
|
-
function isBinaryCapUrn(capUrn,
|
|
2221
|
-
const
|
|
2222
|
-
return
|
|
2225
|
+
function isBinaryCapUrn(capUrn, mediaDefs = []) {
|
|
2226
|
+
const mediaDef = MediaDef.fromCapUrn(capUrn, mediaDefs);
|
|
2227
|
+
return mediaDef.isBinary();
|
|
2223
2228
|
}
|
|
2224
2229
|
|
|
2225
2230
|
/**
|
|
@@ -2227,13 +2232,13 @@ function isBinaryCapUrn(capUrn, mediaSpecs = []) {
|
|
|
2227
2232
|
* Note: This checks for explicit JSON format marker only.
|
|
2228
2233
|
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
2229
2234
|
* @param {CapUrn} capUrn - The cap URN
|
|
2230
|
-
* @param {Array}
|
|
2235
|
+
* @param {Array} mediaDefs - Optional mediaDefs array
|
|
2231
2236
|
* @returns {boolean} True if explicit JSON tag present
|
|
2232
|
-
* @throws {
|
|
2237
|
+
* @throws {MediaDefError} If 'out' tag is missing or spec ID cannot be resolved
|
|
2233
2238
|
*/
|
|
2234
|
-
function isJSONCapUrn(capUrn,
|
|
2235
|
-
const
|
|
2236
|
-
return
|
|
2239
|
+
function isJSONCapUrn(capUrn, mediaDefs = []) {
|
|
2240
|
+
const mediaDef = MediaDef.fromCapUrn(capUrn, mediaDefs);
|
|
2241
|
+
return mediaDef.isJSON();
|
|
2237
2242
|
}
|
|
2238
2243
|
|
|
2239
2244
|
/**
|
|
@@ -2241,13 +2246,13 @@ function isJSONCapUrn(capUrn, mediaSpecs = []) {
|
|
|
2241
2246
|
* Structured data can be serialized as JSON when transmitted as text.
|
|
2242
2247
|
* Throws error if the output spec cannot be resolved - no fallbacks.
|
|
2243
2248
|
* @param {CapUrn} capUrn - The cap URN
|
|
2244
|
-
* @param {Array}
|
|
2249
|
+
* @param {Array} mediaDefs - Optional mediaDefs array
|
|
2245
2250
|
* @returns {boolean} True if structured (map or list)
|
|
2246
|
-
* @throws {
|
|
2251
|
+
* @throws {MediaDefError} If 'out' tag is missing or spec ID cannot be resolved
|
|
2247
2252
|
*/
|
|
2248
|
-
function isStructuredCapUrn(capUrn,
|
|
2249
|
-
const
|
|
2250
|
-
return
|
|
2253
|
+
function isStructuredCapUrn(capUrn, mediaDefs = []) {
|
|
2254
|
+
const mediaDef = MediaDef.fromCapUrn(capUrn, mediaDefs);
|
|
2255
|
+
return mediaDef.isStructured();
|
|
2251
2256
|
}
|
|
2252
2257
|
|
|
2253
2258
|
/**
|
|
@@ -2309,10 +2314,15 @@ const RESERVED_CLI_FLAGS = ['manifest', '--help', '--version', '-v', '-h'];
|
|
|
2309
2314
|
* Argument source - specifies how an argument can be provided
|
|
2310
2315
|
*/
|
|
2311
2316
|
class ArgSource {
|
|
2312
|
-
constructor() {
|
|
2317
|
+
constructor(obj = null) {
|
|
2313
2318
|
this.stdin = null; // string (media URN) or null
|
|
2314
2319
|
this.position = null; // number or null
|
|
2315
2320
|
this.cli_flag = null; // string or null
|
|
2321
|
+
if (obj !== null) {
|
|
2322
|
+
if (obj.stdin !== undefined) this.stdin = obj.stdin;
|
|
2323
|
+
if (obj.position !== undefined) this.position = obj.position;
|
|
2324
|
+
if (obj.cli_flag !== undefined) this.cli_flag = obj.cli_flag;
|
|
2325
|
+
}
|
|
2316
2326
|
}
|
|
2317
2327
|
|
|
2318
2328
|
/**
|
|
@@ -3041,25 +3051,25 @@ class ValidationError extends Error {
|
|
|
3041
3051
|
case 'UnknownArgument':
|
|
3042
3052
|
return `Cap '${capUrn}' does not accept argument '${details.argumentName}' - check capability definition for valid arguments`;
|
|
3043
3053
|
case 'InvalidArgumentType':
|
|
3044
|
-
if (details.
|
|
3054
|
+
if (details.expectedMediaDef) {
|
|
3045
3055
|
const errors = details.schemaErrors ? details.schemaErrors.join(', ') : 'validation failed';
|
|
3046
|
-
return `Cap '${capUrn}' argument '${details.argumentName}' expects
|
|
3056
|
+
return `Cap '${capUrn}' argument '${details.argumentName}' expects media_def '${details.expectedMediaDef}' but ${errors} for value: ${JSON.stringify(details.actualValue)}`;
|
|
3047
3057
|
}
|
|
3048
3058
|
return `Cap '${capUrn}' argument '${details.argumentName}' expects type '${details.expectedType}' but received '${details.actualType}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3049
3059
|
case 'MediaValidationFailed':
|
|
3050
3060
|
return `Cap '${capUrn}' argument '${details.argumentName}' failed validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3051
|
-
case '
|
|
3052
|
-
return `Cap '${capUrn}' argument '${details.argumentName}' failed media
|
|
3061
|
+
case 'MediaDefValidationFailed':
|
|
3062
|
+
return `Cap '${capUrn}' argument '${details.argumentName}' failed media def '${details.mediaUrn}' validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3053
3063
|
case 'InvalidOutputType':
|
|
3054
|
-
if (details.
|
|
3064
|
+
if (details.expectedMediaDef) {
|
|
3055
3065
|
const errors = details.schemaErrors ? details.schemaErrors.join(', ') : 'validation failed';
|
|
3056
|
-
return `Cap '${capUrn}' output expects
|
|
3066
|
+
return `Cap '${capUrn}' output expects media_def '${details.expectedMediaDef}' but ${errors} for value: ${JSON.stringify(details.actualValue)}`;
|
|
3057
3067
|
}
|
|
3058
3068
|
return `Cap '${capUrn}' output expects type '${details.expectedType}' but received '${details.actualType}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3059
3069
|
case 'OutputValidationFailed':
|
|
3060
3070
|
return `Cap '${capUrn}' output failed validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3061
|
-
case '
|
|
3062
|
-
return `Cap '${capUrn}' output failed media
|
|
3071
|
+
case 'OutputMediaDefValidationFailed':
|
|
3072
|
+
return `Cap '${capUrn}' output failed media def '${details.mediaUrn}' validation rule '${details.validationRule}' with value: ${JSON.stringify(details.actualValue)}`;
|
|
3063
3073
|
case 'InvalidCapSchema':
|
|
3064
3074
|
return `Cap '${capUrn}' has invalid schema: ${details.issue}`;
|
|
3065
3075
|
case 'TooManyArguments':
|
|
@@ -3226,12 +3236,12 @@ class InputValidator {
|
|
|
3226
3236
|
*
|
|
3227
3237
|
* @param {Cap} cap
|
|
3228
3238
|
* @param {Array} argValues
|
|
3229
|
-
* @param {Array}
|
|
3239
|
+
* @param {Array} mediaDefs - Media defs the cap's args reference;
|
|
3230
3240
|
* threaded through to `resolveMediaUrn` for schema resolution.
|
|
3231
3241
|
* Required for any cap whose args reference media URNs that
|
|
3232
3242
|
* resolve through the registry.
|
|
3233
3243
|
*/
|
|
3234
|
-
static validatePositionalArguments(cap, argValues,
|
|
3244
|
+
static validatePositionalArguments(cap, argValues, mediaDefs = []) {
|
|
3235
3245
|
const capUrn = cap.urnString();
|
|
3236
3246
|
const args = cap.arguments;
|
|
3237
3247
|
|
|
@@ -3252,7 +3262,7 @@ class InputValidator {
|
|
|
3252
3262
|
});
|
|
3253
3263
|
}
|
|
3254
3264
|
|
|
3255
|
-
InputValidator.validateSingleArgument(cap, args.required[i], argValues[i],
|
|
3265
|
+
InputValidator.validateSingleArgument(cap, args.required[i], argValues[i], mediaDefs);
|
|
3256
3266
|
}
|
|
3257
3267
|
|
|
3258
3268
|
// Validate optional arguments if provided
|
|
@@ -3260,7 +3270,7 @@ class InputValidator {
|
|
|
3260
3270
|
for (let i = 0; i < args.optional.length; i++) {
|
|
3261
3271
|
const argIndex = requiredCount + i;
|
|
3262
3272
|
if (argIndex < argValues.length) {
|
|
3263
|
-
InputValidator.validateSingleArgument(cap, args.optional[i], argValues[argIndex],
|
|
3273
|
+
InputValidator.validateSingleArgument(cap, args.optional[i], argValues[argIndex], mediaDefs);
|
|
3264
3274
|
}
|
|
3265
3275
|
}
|
|
3266
3276
|
}
|
|
@@ -3270,10 +3280,10 @@ class InputValidator {
|
|
|
3270
3280
|
*
|
|
3271
3281
|
* @param {Cap} cap
|
|
3272
3282
|
* @param {Array} namedArgs
|
|
3273
|
-
* @param {Array}
|
|
3283
|
+
* @param {Array} mediaDefs - Media defs the cap's args reference;
|
|
3274
3284
|
* threaded through to `resolveMediaUrn` for schema resolution.
|
|
3275
3285
|
*/
|
|
3276
|
-
static validateNamedArguments(cap, namedArgs,
|
|
3286
|
+
static validateNamedArguments(cap, namedArgs, mediaDefs = []) {
|
|
3277
3287
|
const capUrn = cap.urnString();
|
|
3278
3288
|
const args = cap.arguments;
|
|
3279
3289
|
|
|
@@ -3295,14 +3305,14 @@ class InputValidator {
|
|
|
3295
3305
|
|
|
3296
3306
|
// Validate the provided argument value
|
|
3297
3307
|
const providedValue = providedArgs.get(reqArg.name);
|
|
3298
|
-
InputValidator.validateSingleArgument(cap, reqArg, providedValue,
|
|
3308
|
+
InputValidator.validateSingleArgument(cap, reqArg, providedValue, mediaDefs);
|
|
3299
3309
|
}
|
|
3300
3310
|
|
|
3301
3311
|
// Validate optional arguments if provided
|
|
3302
3312
|
for (const optArg of args.optional) {
|
|
3303
3313
|
if (providedArgs.has(optArg.name)) {
|
|
3304
3314
|
const providedValue = providedArgs.get(optArg.name);
|
|
3305
|
-
InputValidator.validateSingleArgument(cap, optArg, providedValue,
|
|
3315
|
+
InputValidator.validateSingleArgument(cap, optArg, providedValue, mediaDefs);
|
|
3306
3316
|
}
|
|
3307
3317
|
}
|
|
3308
3318
|
|
|
@@ -3324,24 +3334,24 @@ class InputValidator {
|
|
|
3324
3334
|
/**
|
|
3325
3335
|
* Validate a single argument against its definition
|
|
3326
3336
|
* Two-pass validation:
|
|
3327
|
-
* 1. Type validation + media
|
|
3337
|
+
* 1. Type validation + media def validation rules (inherent to semantic type)
|
|
3328
3338
|
*/
|
|
3329
|
-
static validateSingleArgument(cap, argDef, value,
|
|
3330
|
-
// Type validation - returns the resolved
|
|
3331
|
-
const
|
|
3339
|
+
static validateSingleArgument(cap, argDef, value, mediaDefs = []) {
|
|
3340
|
+
// Type validation - returns the resolved MediaDef
|
|
3341
|
+
const mediaDef = InputValidator.validateArgumentType(cap, argDef, value, mediaDefs);
|
|
3332
3342
|
|
|
3333
|
-
// Media
|
|
3334
|
-
if (
|
|
3335
|
-
InputValidator.
|
|
3343
|
+
// Media def validation rules (inherent to the semantic type)
|
|
3344
|
+
if (mediaDef && mediaDef.validation) {
|
|
3345
|
+
InputValidator.validateMediaDefRules(cap, argDef, mediaDef, value);
|
|
3336
3346
|
}
|
|
3337
3347
|
}
|
|
3338
3348
|
|
|
3339
3349
|
/**
|
|
3340
|
-
* Validate argument type using
|
|
3341
|
-
* Resolves spec ID to
|
|
3342
|
-
* @returns {
|
|
3350
|
+
* Validate argument type using MediaDef
|
|
3351
|
+
* Resolves spec ID to MediaDef before validation
|
|
3352
|
+
* @returns {MediaDef|null} The resolved MediaDef
|
|
3343
3353
|
*/
|
|
3344
|
-
static validateArgumentType(cap, argDef, value,
|
|
3354
|
+
static validateArgumentType(cap, argDef, value, mediaDefs = []) {
|
|
3345
3355
|
const capUrn = cap.urnString();
|
|
3346
3356
|
|
|
3347
3357
|
// Get mediaUrn field (now contains a media URN)
|
|
@@ -3351,10 +3361,10 @@ class InputValidator {
|
|
|
3351
3361
|
return null;
|
|
3352
3362
|
}
|
|
3353
3363
|
|
|
3354
|
-
// Resolve media URN to
|
|
3355
|
-
let
|
|
3364
|
+
// Resolve media URN to MediaDef - FAIL HARD if unresolvable
|
|
3365
|
+
let mediaDef;
|
|
3356
3366
|
try {
|
|
3357
|
-
|
|
3367
|
+
mediaDef = resolveMediaUrn(mediaUrn, mediaDefs);
|
|
3358
3368
|
} catch (e) {
|
|
3359
3369
|
throw new ValidationError('InvalidCapSchema', capUrn, {
|
|
3360
3370
|
issue: `Cannot resolve media URN '${mediaUrn}' for argument '${argDef.name}': ${e.message}`
|
|
@@ -3362,56 +3372,56 @@ class InputValidator {
|
|
|
3362
3372
|
}
|
|
3363
3373
|
|
|
3364
3374
|
// For binary media types, expect base64-encoded string
|
|
3365
|
-
if (
|
|
3375
|
+
if (mediaDef.isBinary()) {
|
|
3366
3376
|
if (typeof value !== 'string') {
|
|
3367
3377
|
throw new ValidationError('InvalidArgumentType', capUrn, {
|
|
3368
3378
|
argumentName: argDef.name,
|
|
3369
|
-
|
|
3379
|
+
expectedMediaDef: mediaUrn,
|
|
3370
3380
|
actualValue: value,
|
|
3371
3381
|
schemaErrors: ['Expected base64-encoded string for binary type']
|
|
3372
3382
|
});
|
|
3373
3383
|
}
|
|
3374
|
-
return
|
|
3384
|
+
return mediaDef;
|
|
3375
3385
|
}
|
|
3376
3386
|
|
|
3377
|
-
// If the resolved media
|
|
3378
|
-
if (
|
|
3387
|
+
// If the resolved media def has a local schema, validate against it
|
|
3388
|
+
if (mediaDef.schema) {
|
|
3379
3389
|
// TODO: Full JSON Schema validation would require a JSON Schema library
|
|
3380
3390
|
// For now, skip local schema validation
|
|
3381
3391
|
}
|
|
3382
3392
|
|
|
3383
3393
|
// For types with profile, validate against profile
|
|
3384
|
-
if (
|
|
3385
|
-
const valid = InputValidator.validateAgainstProfile(
|
|
3394
|
+
if (mediaDef.profile) {
|
|
3395
|
+
const valid = InputValidator.validateAgainstProfile(mediaDef.profile, value);
|
|
3386
3396
|
if (!valid) {
|
|
3387
3397
|
throw new ValidationError('InvalidArgumentType', capUrn, {
|
|
3388
3398
|
argumentName: argDef.name,
|
|
3389
|
-
|
|
3399
|
+
expectedMediaDef: mediaUrn,
|
|
3390
3400
|
actualValue: value,
|
|
3391
3401
|
schemaErrors: [`Value does not match profile schema`]
|
|
3392
3402
|
});
|
|
3393
3403
|
}
|
|
3394
3404
|
}
|
|
3395
3405
|
|
|
3396
|
-
return
|
|
3406
|
+
return mediaDef;
|
|
3397
3407
|
}
|
|
3398
3408
|
|
|
3399
3409
|
/**
|
|
3400
|
-
* Validate value against media
|
|
3410
|
+
* Validate value against media def's inherent validation rules (first pass)
|
|
3401
3411
|
* @param {Cap} cap - The capability
|
|
3402
3412
|
* @param {Object} argDef - The argument definition
|
|
3403
|
-
* @param {
|
|
3413
|
+
* @param {MediaDef} mediaDef - The resolved media def
|
|
3404
3414
|
* @param {*} value - The value to validate
|
|
3405
3415
|
*/
|
|
3406
|
-
static
|
|
3416
|
+
static validateMediaDefRules(cap, argDef, mediaDef, value) {
|
|
3407
3417
|
const capUrn = cap.urnString();
|
|
3408
|
-
const validation =
|
|
3409
|
-
const mediaUrn =
|
|
3418
|
+
const validation = mediaDef.validation;
|
|
3419
|
+
const mediaUrn = mediaDef.mediaUrn;
|
|
3410
3420
|
|
|
3411
3421
|
// Min/max validation for numbers
|
|
3412
3422
|
if (typeof value === 'number') {
|
|
3413
3423
|
if (validation.min !== undefined && value < validation.min) {
|
|
3414
|
-
throw new ValidationError('
|
|
3424
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3415
3425
|
argumentName: argDef.name,
|
|
3416
3426
|
mediaUrn: mediaUrn,
|
|
3417
3427
|
validationRule: `min value ${validation.min}`,
|
|
@@ -3419,7 +3429,7 @@ class InputValidator {
|
|
|
3419
3429
|
});
|
|
3420
3430
|
}
|
|
3421
3431
|
if (validation.max !== undefined && value > validation.max) {
|
|
3422
|
-
throw new ValidationError('
|
|
3432
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3423
3433
|
argumentName: argDef.name,
|
|
3424
3434
|
mediaUrn: mediaUrn,
|
|
3425
3435
|
validationRule: `max value ${validation.max}`,
|
|
@@ -3432,7 +3442,7 @@ class InputValidator {
|
|
|
3432
3442
|
if (typeof value === 'string' || Array.isArray(value)) {
|
|
3433
3443
|
const length = value.length;
|
|
3434
3444
|
if (validation.min_length !== undefined && length < validation.min_length) {
|
|
3435
|
-
throw new ValidationError('
|
|
3445
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3436
3446
|
argumentName: argDef.name,
|
|
3437
3447
|
mediaUrn: mediaUrn,
|
|
3438
3448
|
validationRule: `min length ${validation.min_length}`,
|
|
@@ -3440,7 +3450,7 @@ class InputValidator {
|
|
|
3440
3450
|
});
|
|
3441
3451
|
}
|
|
3442
3452
|
if (validation.max_length !== undefined && length > validation.max_length) {
|
|
3443
|
-
throw new ValidationError('
|
|
3453
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3444
3454
|
argumentName: argDef.name,
|
|
3445
3455
|
mediaUrn: mediaUrn,
|
|
3446
3456
|
validationRule: `max length ${validation.max_length}`,
|
|
@@ -3453,7 +3463,7 @@ class InputValidator {
|
|
|
3453
3463
|
if (typeof value === 'string' && validation.pattern) {
|
|
3454
3464
|
const regex = new RegExp(validation.pattern);
|
|
3455
3465
|
if (!regex.test(value)) {
|
|
3456
|
-
throw new ValidationError('
|
|
3466
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3457
3467
|
argumentName: argDef.name,
|
|
3458
3468
|
mediaUrn: mediaUrn,
|
|
3459
3469
|
validationRule: `pattern ${validation.pattern}`,
|
|
@@ -3465,7 +3475,7 @@ class InputValidator {
|
|
|
3465
3475
|
// Allowed values validation
|
|
3466
3476
|
if (validation.allowed_values && Array.isArray(validation.allowed_values)) {
|
|
3467
3477
|
if (!validation.allowed_values.includes(value)) {
|
|
3468
|
-
throw new ValidationError('
|
|
3478
|
+
throw new ValidationError('MediaDefValidationFailed', capUrn, {
|
|
3469
3479
|
argumentName: argDef.name,
|
|
3470
3480
|
mediaUrn: mediaUrn,
|
|
3471
3481
|
validationRule: `allowed values [${validation.allowed_values.join(', ')}]`,
|
|
@@ -3536,32 +3546,32 @@ class InputValidator {
|
|
|
3536
3546
|
*/
|
|
3537
3547
|
class OutputValidator {
|
|
3538
3548
|
/**
|
|
3539
|
-
* Validate output against cap output schema using
|
|
3549
|
+
* Validate output against cap output schema using MediaDef.
|
|
3540
3550
|
*
|
|
3541
3551
|
* @param {Cap} cap
|
|
3542
3552
|
* @param {*} output
|
|
3543
|
-
* @param {Array}
|
|
3553
|
+
* @param {Array} mediaDefs - Media defs the cap output references;
|
|
3544
3554
|
* threaded through to `resolveMediaUrn` for schema resolution.
|
|
3545
3555
|
*/
|
|
3546
|
-
static validateOutput(cap, output,
|
|
3556
|
+
static validateOutput(cap, output, mediaDefs = []) {
|
|
3547
3557
|
const outputDef = cap.output;
|
|
3548
3558
|
|
|
3549
3559
|
if (!outputDef) return; // No output definition to validate against
|
|
3550
3560
|
|
|
3551
|
-
// Type validation - returns the resolved
|
|
3552
|
-
const
|
|
3561
|
+
// Type validation - returns the resolved MediaDef
|
|
3562
|
+
const mediaDef = OutputValidator.validateOutputType(cap, outputDef, output, mediaDefs);
|
|
3553
3563
|
|
|
3554
|
-
// Media
|
|
3555
|
-
if (
|
|
3556
|
-
OutputValidator.
|
|
3564
|
+
// Media def validation rules (inherent to the semantic type)
|
|
3565
|
+
if (mediaDef && mediaDef.validation) {
|
|
3566
|
+
OutputValidator.validateOutputMediaDefRules(cap, mediaDef, output);
|
|
3557
3567
|
}
|
|
3558
3568
|
}
|
|
3559
3569
|
|
|
3560
3570
|
/**
|
|
3561
|
-
* Validate output type using
|
|
3562
|
-
* @returns {
|
|
3571
|
+
* Validate output type using MediaDef
|
|
3572
|
+
* @returns {MediaDef|null} The resolved MediaDef
|
|
3563
3573
|
*/
|
|
3564
|
-
static validateOutputType(cap, outputDef, value,
|
|
3574
|
+
static validateOutputType(cap, outputDef, value, mediaDefs = []) {
|
|
3565
3575
|
const capUrn = cap.urnString();
|
|
3566
3576
|
|
|
3567
3577
|
// Get mediaUrn field (now contains a media URN)
|
|
@@ -3571,10 +3581,10 @@ class OutputValidator {
|
|
|
3571
3581
|
return null;
|
|
3572
3582
|
}
|
|
3573
3583
|
|
|
3574
|
-
// Resolve media URN to
|
|
3575
|
-
let
|
|
3584
|
+
// Resolve media URN to MediaDef - FAIL HARD if unresolvable
|
|
3585
|
+
let mediaDef;
|
|
3576
3586
|
try {
|
|
3577
|
-
|
|
3587
|
+
mediaDef = resolveMediaUrn(mediaUrn, mediaDefs);
|
|
3578
3588
|
} catch (e) {
|
|
3579
3589
|
throw new ValidationError('InvalidCapSchema', capUrn, {
|
|
3580
3590
|
issue: `Cannot resolve media URN '${mediaUrn}' for output: ${e.message}`
|
|
@@ -3582,57 +3592,57 @@ class OutputValidator {
|
|
|
3582
3592
|
}
|
|
3583
3593
|
|
|
3584
3594
|
// For binary media types, expect base64-encoded string
|
|
3585
|
-
if (
|
|
3595
|
+
if (mediaDef.isBinary()) {
|
|
3586
3596
|
if (typeof value !== 'string') {
|
|
3587
3597
|
throw new ValidationError('InvalidOutputType', capUrn, {
|
|
3588
|
-
|
|
3598
|
+
expectedMediaDef: mediaUrn,
|
|
3589
3599
|
actualValue: value,
|
|
3590
3600
|
schemaErrors: ['Expected base64-encoded string for binary type']
|
|
3591
3601
|
});
|
|
3592
3602
|
}
|
|
3593
|
-
return
|
|
3603
|
+
return mediaDef;
|
|
3594
3604
|
}
|
|
3595
3605
|
|
|
3596
|
-
// If the resolved media
|
|
3597
|
-
if (
|
|
3606
|
+
// If the resolved media def has a local schema, validate against it
|
|
3607
|
+
if (mediaDef.schema) {
|
|
3598
3608
|
// TODO: Full JSON Schema validation would require a JSON Schema library
|
|
3599
3609
|
// For now, skip local schema validation
|
|
3600
3610
|
}
|
|
3601
3611
|
|
|
3602
3612
|
// For types with profile, validate against profile
|
|
3603
|
-
if (
|
|
3604
|
-
const valid = InputValidator.validateAgainstProfile(
|
|
3613
|
+
if (mediaDef.profile) {
|
|
3614
|
+
const valid = InputValidator.validateAgainstProfile(mediaDef.profile, value);
|
|
3605
3615
|
if (!valid) {
|
|
3606
3616
|
throw new ValidationError('InvalidOutputType', capUrn, {
|
|
3607
|
-
|
|
3617
|
+
expectedMediaDef: mediaUrn,
|
|
3608
3618
|
actualValue: value,
|
|
3609
3619
|
schemaErrors: [`Value does not match profile schema`]
|
|
3610
3620
|
});
|
|
3611
3621
|
}
|
|
3612
3622
|
}
|
|
3613
3623
|
|
|
3614
|
-
return
|
|
3624
|
+
return mediaDef;
|
|
3615
3625
|
}
|
|
3616
3626
|
|
|
3617
3627
|
/**
|
|
3618
|
-
* Validate output against media
|
|
3628
|
+
* Validate output against media def's inherent validation rules (first pass)
|
|
3619
3629
|
*/
|
|
3620
|
-
static
|
|
3630
|
+
static validateOutputMediaDefRules(cap, mediaDef, value) {
|
|
3621
3631
|
const capUrn = cap.urnString();
|
|
3622
|
-
const validation =
|
|
3623
|
-
const mediaUrn =
|
|
3632
|
+
const validation = mediaDef.validation;
|
|
3633
|
+
const mediaUrn = mediaDef.mediaUrn;
|
|
3624
3634
|
|
|
3625
3635
|
// Min/max validation for numbers
|
|
3626
3636
|
if (typeof value === 'number') {
|
|
3627
3637
|
if (validation.min !== undefined && value < validation.min) {
|
|
3628
|
-
throw new ValidationError('
|
|
3638
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3629
3639
|
mediaUrn: mediaUrn,
|
|
3630
3640
|
validationRule: `min value ${validation.min}`,
|
|
3631
3641
|
actualValue: value
|
|
3632
3642
|
});
|
|
3633
3643
|
}
|
|
3634
3644
|
if (validation.max !== undefined && value > validation.max) {
|
|
3635
|
-
throw new ValidationError('
|
|
3645
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3636
3646
|
mediaUrn: mediaUrn,
|
|
3637
3647
|
validationRule: `max value ${validation.max}`,
|
|
3638
3648
|
actualValue: value
|
|
@@ -3643,14 +3653,14 @@ class OutputValidator {
|
|
|
3643
3653
|
// Length validation for strings
|
|
3644
3654
|
if (typeof value === 'string') {
|
|
3645
3655
|
if (validation.min_length !== undefined && value.length < validation.min_length) {
|
|
3646
|
-
throw new ValidationError('
|
|
3656
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3647
3657
|
mediaUrn: mediaUrn,
|
|
3648
3658
|
validationRule: `min length ${validation.min_length}`,
|
|
3649
3659
|
actualValue: value
|
|
3650
3660
|
});
|
|
3651
3661
|
}
|
|
3652
3662
|
if (validation.max_length !== undefined && value.length > validation.max_length) {
|
|
3653
|
-
throw new ValidationError('
|
|
3663
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3654
3664
|
mediaUrn: mediaUrn,
|
|
3655
3665
|
validationRule: `max length ${validation.max_length}`,
|
|
3656
3666
|
actualValue: value
|
|
@@ -3662,7 +3672,7 @@ class OutputValidator {
|
|
|
3662
3672
|
if (typeof value === 'string' && validation.pattern) {
|
|
3663
3673
|
const regex = new RegExp(validation.pattern);
|
|
3664
3674
|
if (!regex.test(value)) {
|
|
3665
|
-
throw new ValidationError('
|
|
3675
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3666
3676
|
mediaUrn: mediaUrn,
|
|
3667
3677
|
validationRule: `pattern ${validation.pattern}`,
|
|
3668
3678
|
actualValue: value
|
|
@@ -3673,7 +3683,7 @@ class OutputValidator {
|
|
|
3673
3683
|
// Allowed values validation
|
|
3674
3684
|
if (validation.allowed_values && Array.isArray(validation.allowed_values)) {
|
|
3675
3685
|
if (!validation.allowed_values.includes(value)) {
|
|
3676
|
-
throw new ValidationError('
|
|
3686
|
+
throw new ValidationError('OutputMediaDefValidationFailed', capUrn, {
|
|
3677
3687
|
mediaUrn: mediaUrn,
|
|
3678
3688
|
validationRule: `allowed values [${validation.allowed_values.join(', ')}]`,
|
|
3679
3689
|
actualValue: value
|
|
@@ -6140,7 +6150,7 @@ class FabricRegistryEntry {
|
|
|
6140
6150
|
}
|
|
6141
6151
|
|
|
6142
6152
|
/**
|
|
6143
|
-
* A media
|
|
6153
|
+
* A media def entry from the registry.
|
|
6144
6154
|
*
|
|
6145
6155
|
* Wire shape mirrors the per-URN objects published at
|
|
6146
6156
|
* <base>/media/<sha256(canonical-urn)>
|
|
@@ -6182,7 +6192,7 @@ async function sha256Hex(s) {
|
|
|
6182
6192
|
}
|
|
6183
6193
|
|
|
6184
6194
|
/**
|
|
6185
|
-
* Client for fetching and caching capabilities and media
|
|
6195
|
+
* Client for fetching and caching capabilities and media defs.
|
|
6186
6196
|
*
|
|
6187
6197
|
* Reads from `<baseUrl>/api/capabilities` (the flat catalogue) and
|
|
6188
6198
|
* `<baseUrl>/{caps,media}/<sha256>` (per-URN).
|
|
@@ -6274,7 +6284,7 @@ class FabricRegistryClient {
|
|
|
6274
6284
|
}
|
|
6275
6285
|
|
|
6276
6286
|
/**
|
|
6277
|
-
* Lookup a single media
|
|
6287
|
+
* Lookup a single media def by URN.
|
|
6278
6288
|
*
|
|
6279
6289
|
* Canonicalises through MediaUrn and looks up by SHA-256 hash.
|
|
6280
6290
|
*
|
|
@@ -6377,9 +6387,9 @@ module.exports = {
|
|
|
6377
6387
|
CapValidator,
|
|
6378
6388
|
validateCapArgs,
|
|
6379
6389
|
RESERVED_CLI_FLAGS,
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6390
|
+
MediaDef,
|
|
6391
|
+
MediaDefError,
|
|
6392
|
+
MediaDefErrorCodes,
|
|
6383
6393
|
isBinaryCapUrn,
|
|
6384
6394
|
isJSONCapUrn,
|
|
6385
6395
|
isStructuredCapUrn,
|
|
@@ -6387,9 +6397,9 @@ module.exports = {
|
|
|
6387
6397
|
buildExtensionIndex,
|
|
6388
6398
|
mediaUrnsForExtension,
|
|
6389
6399
|
getExtensionMappings,
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6400
|
+
validateNoMediaDefRedefinition,
|
|
6401
|
+
validateNoMediaDefRedefinitionSync,
|
|
6402
|
+
validateNoMediaDefDuplicates,
|
|
6393
6403
|
getSchemaBaseURL,
|
|
6394
6404
|
getProfileURL,
|
|
6395
6405
|
MEDIA_STRING,
|
|
@@ -6486,12 +6496,12 @@ module.exports = {
|
|
|
6486
6496
|
MEDIA_CAP_URN,
|
|
6487
6497
|
MEDIA_MEDIA_URN,
|
|
6488
6498
|
MEDIA_CAP_DEFINITION,
|
|
6489
|
-
|
|
6499
|
+
MEDIA_MEDIA_DEFINITION,
|
|
6490
6500
|
// Standard cap URN constants
|
|
6491
6501
|
CAP_IDENTITY,
|
|
6492
6502
|
CAP_ADAPTER_SELECTION,
|
|
6493
6503
|
CAP_LOOKUP_CAP_FABRIC,
|
|
6494
|
-
|
|
6504
|
+
CAP_LOOKUP_MEDIA_DEF_FABRIC,
|
|
6495
6505
|
// Cap execution result
|
|
6496
6506
|
CapResult,
|
|
6497
6507
|
// Unified argument type
|