Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 212 lines 5.5 kB view raw
1import type { Lexicons } from "../lexicons.ts"; 2import { 3 isDiscriminatedObject, 4 isObj, 5 type LexArray, 6 type LexRefVariant, 7 type LexUserType, 8 ValidationError, 9 type ValidationResult, 10} from "../types.ts"; 11import { toLexUri } from "../util.ts"; 12import { blob } from "./blob.ts"; 13import { validate as validatePrimitive } from "./primitives.ts"; 14 15export function validate( 16 lexicons: Lexicons, 17 path: string, 18 def: LexUserType, 19 value: unknown, 20): ValidationResult { 21 switch (def.type) { 22 case "object": 23 return object(lexicons, path, def, value); 24 case "array": 25 return array(lexicons, path, def, value); 26 case "blob": 27 return blob(path, value); 28 default: 29 return validatePrimitive(path, def, value); 30 } 31} 32 33export function array( 34 lexicons: Lexicons, 35 path: string, 36 def: LexArray, 37 value: unknown, 38): ValidationResult { 39 // type 40 if (!Array.isArray(value)) { 41 return { 42 success: false, 43 error: new ValidationError(`${path} must be an array`), 44 }; 45 } 46 47 // maxLength 48 if (typeof def.maxLength === "number") { 49 if ((value as Array<unknown>).length > def.maxLength) { 50 return { 51 success: false, 52 error: new ValidationError( 53 `${path} must not have more than ${def.maxLength} elements`, 54 ), 55 }; 56 } 57 } 58 59 // minLength 60 if (typeof def.minLength === "number") { 61 if ((value as Array<unknown>).length < def.minLength) { 62 return { 63 success: false, 64 error: new ValidationError( 65 `${path} must not have fewer than ${def.minLength} elements`, 66 ), 67 }; 68 } 69 } 70 71 // items 72 const itemsDef = def.items; 73 for (let i = 0; i < (value as Array<unknown>).length; i++) { 74 const itemValue = value[i]; 75 const itemPath = `${path}/${i}`; 76 const res = validateOneOf(lexicons, itemPath, itemsDef, itemValue); 77 if (!res.success) { 78 return res; 79 } 80 } 81 82 return { success: true, value }; 83} 84 85export function object( 86 lexicons: Lexicons, 87 path: string, 88 def: LexUserType, 89 value: unknown, 90): ValidationResult { 91 // type 92 if (!isObj(value)) { 93 return { 94 success: false, 95 error: new ValidationError(`${path} must be an object`), 96 }; 97 } 98 99 // properties 100 let resultValue = value as Record<string, unknown>; 101 if ("properties" in def && def.properties != null) { 102 for (const key in def.properties) { 103 const keyValue = resultValue[key]; 104 if (keyValue === null && def.nullable?.includes(key)) { 105 continue; 106 } 107 const propDef = def.properties[key]; 108 if (keyValue === undefined && !def.required?.includes(key)) { 109 // Fast path for non-required undefined props. 110 if ( 111 propDef.type === "integer" || 112 propDef.type === "boolean" || 113 propDef.type === "string" 114 ) { 115 if (propDef.default === undefined) { 116 continue; 117 } 118 } else { 119 // Other types have no defaults. 120 continue; 121 } 122 } 123 const propPath = `${path}/${key}`; 124 const validated = validateOneOf(lexicons, propPath, propDef, keyValue); 125 const propValue = validated.success ? validated.value : keyValue; 126 127 // Return error for bad validation, giving required rule precedence 128 if (propValue === undefined) { 129 if (def.required?.includes(key)) { 130 return { 131 success: false, 132 error: new ValidationError( 133 `${path} must have the property "${key}"`, 134 ), 135 }; 136 } 137 } else { 138 if (!validated.success) { 139 return validated; 140 } 141 } 142 143 // Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value 144 if (propValue !== keyValue) { 145 if (resultValue === value) { 146 // Lazy shallow clone 147 resultValue = { ...value }; 148 } 149 resultValue[key] = propValue; 150 } 151 } 152 } 153 154 return { success: true, value: resultValue }; 155} 156 157export function validateOneOf( 158 lexicons: Lexicons, 159 path: string, 160 def: LexRefVariant | LexUserType, 161 value: unknown, 162 mustBeObj = false, // this is the only type constraint we need currently (used by xrpc body schema validators) 163): ValidationResult { 164 let concreteDef: LexUserType; 165 166 if (def.type === "union") { 167 if (!isDiscriminatedObject(value)) { 168 return { 169 success: false, 170 error: new ValidationError( 171 `${path} must be an object which includes the "$type" property`, 172 ), 173 }; 174 } 175 if (!refsContainType(def.refs, value.$type)) { 176 if (def.closed) { 177 return { 178 success: false, 179 error: new ValidationError( 180 `${path} $type must be one of ${def.refs.join(", ")}`, 181 ), 182 }; 183 } 184 return { success: true, value }; 185 } else { 186 concreteDef = lexicons.getDefOrThrow(value.$type); 187 } 188 } else if (def.type === "ref") { 189 concreteDef = lexicons.getDefOrThrow(def.ref); 190 } else { 191 concreteDef = def; 192 } 193 194 return mustBeObj 195 ? object(lexicons, path, concreteDef, value) 196 : validate(lexicons, path, concreteDef, value); 197} 198 199// to avoid bugs like #0189 this needs to handle both 200// explicit and implicit #main 201const refsContainType = (refs: string[], type: string) => { 202 const lexUri = toLexUri(type); 203 if (refs.includes(lexUri)) { 204 return true; 205 } 206 207 if (lexUri.endsWith("#main")) { 208 return refs.includes(lexUri.slice(0, -5)); 209 } else { 210 return !lexUri.includes("#") && refs.includes(`${lexUri}#main`); 211 } 212};