@atproto/lexicon
Advanced tools
Comparing version 0.0.4 to 0.1.0
export * from './types'; | ||
export * from './lexicons'; | ||
export * from './blob-refs'; | ||
export * from './serialize'; |
@@ -12,6 +12,8 @@ import { LexiconDoc, LexUserType, ValidationResult } from './types'; | ||
validate(lexUri: string, value: unknown): ValidationResult; | ||
assertValidRecord(lexUri: string, value: unknown): void; | ||
assertValidXrpcParams(lexUri: string, value: unknown): void; | ||
assertValidXrpcInput(lexUri: string, value: unknown): void; | ||
assertValidXrpcOutput(lexUri: string, value: unknown): void; | ||
assertValidRecord(lexUri: string, value: unknown): unknown; | ||
assertValidXrpcParams(lexUri: string, value: unknown): unknown; | ||
assertValidXrpcInput(lexUri: string, value: unknown): unknown; | ||
assertValidXrpcOutput(lexUri: string, value: unknown): unknown; | ||
assertValidXrpcMessage<T = unknown>(lexUri: string, value: unknown): T; | ||
resolveLexUri(lexUri: string, ref: string): string; | ||
} |
@@ -5,3 +5,3 @@ import { Lexicons } from './lexicons'; | ||
export declare function validateOneOf(lexicons: Lexicons, path: string, def: LexRefVariant | LexUserType, value: unknown, mustBeObj?: boolean): ValidationResult; | ||
export declare function assertValidOneOf(lexicons: Lexicons, path: string, def: LexRefVariant | LexUserType, value: unknown, mustBeObj?: boolean): void; | ||
export declare function assertValidOneOf(lexicons: Lexicons, path: string, def: LexRefVariant | LexUserType, value: unknown, mustBeObj?: boolean): unknown; | ||
export declare function toConcreteTypes(lexicons: Lexicons, def: LexRefVariant | LexUserType): LexUserType[]; |
import { Lexicons } from './lexicons'; | ||
import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types'; | ||
export declare function assertValidRecord(lexicons: Lexicons, def: LexRecord, value: unknown): void; | ||
export declare function assertValidXrpcParams(lexicons: Lexicons, def: LexXrpcProcedure | LexXrpcQuery, value: unknown): void; | ||
export declare function assertValidXrpcInput(lexicons: Lexicons, def: LexXrpcProcedure, value: unknown): void; | ||
export declare function assertValidXrpcOutput(lexicons: Lexicons, def: LexXrpcProcedure | LexXrpcQuery, value: unknown): void; | ||
import { LexRecord, LexXrpcProcedure, LexXrpcQuery, LexXrpcSubscription } from './types'; | ||
export declare function assertValidRecord(lexicons: Lexicons, def: LexRecord, value: unknown): unknown; | ||
export declare function assertValidXrpcParams(lexicons: Lexicons, def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription, value: unknown): unknown; | ||
export declare function assertValidXrpcInput(lexicons: Lexicons, def: LexXrpcProcedure, value: unknown): unknown; | ||
export declare function assertValidXrpcOutput(lexicons: Lexicons, def: LexXrpcProcedure | LexXrpcQuery, value: unknown): unknown; | ||
export declare function assertValidXrpcMessage(lexicons: Lexicons, def: LexXrpcSubscription, value: unknown): unknown; |
import { Lexicons } from '../lexicons'; | ||
import { LexUserType, ValidationResult } from '../types'; | ||
export declare function blob(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function image(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function video(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function audio(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; |
import { Lexicons } from '../lexicons'; | ||
import { LexUserType, ValidationResult } from '../types'; | ||
import { LexArray, LexUserType, ValidationResult } from '../types'; | ||
export declare function validate(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function array(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function array(lexicons: Lexicons, path: string, def: LexArray, value: unknown): ValidationResult; | ||
export declare function object(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; |
@@ -5,6 +5,7 @@ import { Lexicons } from '../lexicons'; | ||
export declare function boolean(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function number(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function float(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function integer(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function string(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function datetime(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function bytes(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function cidLink(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; | ||
export declare function unknown(lexicons: Lexicons, path: string, def: LexUserType, value: unknown): ValidationResult; |
import { Lexicons } from '../lexicons'; | ||
import { LexXrpcParameters, ValidationResult } from '../types'; | ||
export declare function params(lexicons: Lexicons, path: string, def: LexXrpcParameters, value: unknown): ValidationResult; | ||
export declare function params(lexicons: Lexicons, path: string, def: LexXrpcParameters, val: unknown): ValidationResult; |
{ | ||
"name": "@atproto/lexicon", | ||
"version": "0.0.4", | ||
"version": "0.1.0", | ||
"main": "dist/index.js", | ||
@@ -22,6 +22,10 @@ "scripts": { | ||
"dependencies": { | ||
"@atproto/common-web": "*", | ||
"@atproto/identifier": "*", | ||
"@atproto/nsid": "*", | ||
"@atproto/uri": "*", | ||
"iso-datestring-validator": "^2.2.2", | ||
"multiformats": "^9.6.4", | ||
"zod": "^3.14.2" | ||
} | ||
} |
export * from './types' | ||
export * from './lexicons' | ||
export * from './blob-refs' | ||
export * from './serialize' |
@@ -16,2 +16,3 @@ import { ZodError } from 'zod' | ||
hasProp, | ||
LexXrpcSubscription, | ||
} from './types' | ||
@@ -23,2 +24,3 @@ import { | ||
assertValidXrpcOutput, | ||
assertValidXrpcMessage, | ||
} from './validation' | ||
@@ -163,3 +165,3 @@ import { toLexUri } from './util' | ||
} | ||
assertValidRecord(this, def as LexRecord, value) | ||
return assertValidRecord(this, def as LexRecord, value) | ||
} | ||
@@ -172,4 +174,12 @@ | ||
lexUri = toLexUri(lexUri) | ||
const def = this.getDefOrThrow(lexUri, ['query', 'procedure']) | ||
assertValidXrpcParams(this, def as LexXrpcProcedure | LexXrpcQuery, value) | ||
const def = this.getDefOrThrow(lexUri, [ | ||
'query', | ||
'procedure', | ||
'subscription', | ||
]) | ||
return assertValidXrpcParams( | ||
this, | ||
def as LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription, | ||
value, | ||
) | ||
} | ||
@@ -183,3 +193,3 @@ | ||
const def = this.getDefOrThrow(lexUri, ['procedure']) | ||
assertValidXrpcInput(this, def as LexXrpcProcedure, value) | ||
return assertValidXrpcInput(this, def as LexXrpcProcedure, value) | ||
} | ||
@@ -193,4 +203,25 @@ | ||
const def = this.getDefOrThrow(lexUri, ['query', 'procedure']) | ||
assertValidXrpcOutput(this, def as LexXrpcProcedure | LexXrpcQuery, value) | ||
return assertValidXrpcOutput( | ||
this, | ||
def as LexXrpcProcedure | LexXrpcQuery, | ||
value, | ||
) | ||
} | ||
/** | ||
* Validate xrpc subscription message and throw on any error. | ||
*/ | ||
assertValidXrpcMessage<T = unknown>(lexUri: string, value: unknown): T { | ||
lexUri = toLexUri(lexUri) | ||
const def = this.getDefOrThrow(lexUri, ['subscription']) | ||
return assertValidXrpcMessage(this, def as LexXrpcSubscription, value) as T | ||
} | ||
/** | ||
* Resolve a lex uri given a ref | ||
*/ | ||
resolveLexUri(lexUri: string, ref: string) { | ||
lexUri = toLexUri(lexUri) | ||
return toLexUri(ref, lexUri) | ||
} | ||
} | ||
@@ -197,0 +228,0 @@ |
144
src/types.ts
@@ -15,4 +15,4 @@ import { z } from 'zod' | ||
export const lexNumber = z.object({ | ||
type: z.literal('number'), | ||
export const lexFloat = z.object({ | ||
type: z.literal('float'), | ||
description: z.string().optional(), | ||
@@ -25,3 +25,3 @@ default: z.number().optional(), | ||
}) | ||
export type LexNumber = z.infer<typeof lexNumber> | ||
export type LexFloat = z.infer<typeof lexFloat> | ||
@@ -39,4 +39,17 @@ export const lexInteger = z.object({ | ||
export const lexStringFormat = z.enum([ | ||
'datetime', | ||
'uri', | ||
'at-uri', | ||
'did', | ||
'handle', | ||
'at-identifier', | ||
'nsid', | ||
'cid', | ||
]) | ||
export type LexStringFormat = z.infer<typeof lexStringFormat> | ||
export const lexString = z.object({ | ||
type: z.literal('string'), | ||
format: lexStringFormat.optional(), | ||
description: z.string().optional(), | ||
@@ -46,2 +59,4 @@ default: z.string().optional(), | ||
maxLength: z.number().int().optional(), | ||
minGraphemes: z.number().int().optional(), | ||
maxGraphemes: z.number().int().optional(), | ||
enum: z.string().array().optional(), | ||
@@ -53,8 +68,2 @@ const: z.string().optional(), | ||
export const lexDatetime = z.object({ | ||
type: z.literal('datetime'), | ||
description: z.string().optional(), | ||
}) | ||
export type LexDatetime = z.infer<typeof lexDatetime> | ||
export const lexUnknown = z.object({ | ||
@@ -68,6 +77,5 @@ type: z.literal('unknown'), | ||
lexBoolean, | ||
lexNumber, | ||
lexFloat, | ||
lexInteger, | ||
lexString, | ||
lexDatetime, | ||
lexUnknown, | ||
@@ -77,2 +85,22 @@ ]) | ||
// ipld types | ||
// = | ||
export const lexBytes = z.object({ | ||
type: z.literal('bytes'), | ||
description: z.string().optional(), | ||
maxLength: z.number().optional(), | ||
minLength: z.number().optional(), | ||
}) | ||
export type LexBytes = z.infer<typeof lexBytes> | ||
export const lexCidLink = z.object({ | ||
type: z.literal('cid-link'), | ||
description: z.string().optional(), | ||
}) | ||
export type LexCidLink = z.infer<typeof lexCidLink> | ||
export const lexIpldType = z.union([lexBytes, lexCidLink]) | ||
export type LexIpldType = z.infer<typeof lexIpldType> | ||
// references | ||
@@ -110,35 +138,2 @@ // = | ||
export const lexImage = z.object({ | ||
type: z.literal('image'), | ||
description: z.string().optional(), | ||
accept: z.string().array().optional(), | ||
maxSize: z.number().optional(), | ||
maxWidth: z.number().int().optional(), | ||
maxHeight: z.number().int().optional(), | ||
}) | ||
export type LexImage = z.infer<typeof lexImage> | ||
export const lexVideo = z.object({ | ||
type: z.literal('video'), | ||
description: z.string().optional(), | ||
accept: z.string().array().optional(), | ||
maxSize: z.number().optional(), | ||
maxWidth: z.number().int().optional(), | ||
maxHeight: z.number().int().optional(), | ||
maxLength: z.number().int().optional(), | ||
}) | ||
export type LexVideo = z.infer<typeof lexVideo> | ||
export const lexAudio = z.object({ | ||
type: z.literal('audio'), | ||
description: z.string().optional(), | ||
accept: z.string().array().optional(), | ||
maxSize: z.number().optional(), | ||
maxLength: z.number().int().optional(), | ||
}) | ||
export type LexAudio = z.infer<typeof lexAudio> | ||
export const lexBlobVariant = z.union([lexBlob, lexImage, lexVideo, lexAudio]) | ||
export type LexBlobVariant = z.infer<typeof lexBlobVariant> | ||
// complex types | ||
@@ -150,3 +145,3 @@ // = | ||
description: z.string().optional(), | ||
items: z.union([lexPrimitive, lexBlobVariant, lexRefVariant]), | ||
items: z.union([lexPrimitive, lexIpldType, lexBlob, lexRefVariant]), | ||
minLength: z.number().int().optional(), | ||
@@ -157,2 +152,9 @@ maxLength: z.number().int().optional(), | ||
export const lexPrimitiveArray = lexArray.merge( | ||
z.object({ | ||
items: lexPrimitive, | ||
}), | ||
) | ||
export type LexPrimitiveArray = z.infer<typeof lexPrimitiveArray> | ||
export const lexToken = z.object({ | ||
@@ -168,4 +170,7 @@ type: z.literal('token'), | ||
required: z.string().array().optional(), | ||
nullable: z.string().array().optional(), | ||
properties: z | ||
.record(z.union([lexRefVariant, lexArray, lexBlobVariant, lexPrimitive])) | ||
.record( | ||
z.union([lexRefVariant, lexIpldType, lexArray, lexBlob, lexPrimitive]), | ||
) | ||
.optional(), | ||
@@ -182,3 +187,3 @@ }) | ||
required: z.string().array().optional(), | ||
properties: z.record(lexPrimitive), | ||
properties: z.record(z.union([lexPrimitive, lexPrimitiveArray])), | ||
}) | ||
@@ -194,2 +199,10 @@ export type LexXrpcParameters = z.infer<typeof lexXrpcParameters> | ||
export const lexXrpcSubscriptionMessage = z.object({ | ||
description: z.string().optional(), | ||
schema: z.union([lexRefVariant, lexObject]).optional(), | ||
}) | ||
export type LexXrpcSubscriptionMessage = z.infer< | ||
typeof lexXrpcSubscriptionMessage | ||
> | ||
export const lexXrpcError = z.object({ | ||
@@ -220,2 +233,12 @@ name: z.string(), | ||
export const lexXrpcSubscription = z.object({ | ||
type: z.literal('subscription'), | ||
description: z.string().optional(), | ||
parameters: lexXrpcParameters.optional(), | ||
message: lexXrpcSubscriptionMessage.optional(), | ||
infos: lexXrpcError.array().optional(), | ||
errors: lexXrpcError.array().optional(), | ||
}) | ||
export type LexXrpcSubscription = z.infer<typeof lexXrpcSubscription> | ||
// database | ||
@@ -240,7 +263,5 @@ // = | ||
lexXrpcProcedure, | ||
lexXrpcSubscription, | ||
lexBlob, | ||
lexImage, | ||
lexVideo, | ||
lexAudio, | ||
@@ -252,6 +273,7 @@ lexArray, | ||
lexBoolean, | ||
lexNumber, | ||
lexFloat, | ||
lexInteger, | ||
lexString, | ||
lexDatetime, | ||
lexBytes, | ||
lexCidLink, | ||
lexUnknown, | ||
@@ -278,7 +300,8 @@ ]) | ||
def.type === 'procedure' || | ||
def.type === 'query') | ||
def.type === 'query' || | ||
def.type === 'subscription') | ||
) { | ||
ctx.addIssue({ | ||
code: z.ZodIssueCode.custom, | ||
message: `Records, procedures, and queries must be the main definition.`, | ||
message: `Records, procedures, queries, and subscriptions must be the main definition.`, | ||
}) | ||
@@ -328,6 +351,11 @@ } | ||
export interface ValidationResult { | ||
success: boolean | ||
error?: ValidationError | ||
} | ||
export type ValidationResult = | ||
| { | ||
success: true | ||
value: unknown | ||
} | ||
| { | ||
success: false | ||
error: ValidationError | ||
} | ||
@@ -334,0 +362,0 @@ export class ValidationError extends Error {} |
@@ -52,3 +52,3 @@ import { Lexicons } from './lexicons' | ||
} | ||
return { success: true } | ||
return { success: true, value } | ||
} else { | ||
@@ -92,5 +92,4 @@ concreteDefs = toConcreteTypes(lexicons, { | ||
const res = validateOneOf(lexicons, path, def, value, mustBeObj) | ||
if (!res.success) { | ||
throw res.error | ||
} | ||
if (!res.success) throw res.error | ||
return res.value | ||
} | ||
@@ -97,0 +96,0 @@ |
import { Lexicons } from './lexicons' | ||
import { LexRecord, LexXrpcProcedure, LexXrpcQuery } from './types' | ||
import { | ||
LexRecord, | ||
LexXrpcProcedure, | ||
LexXrpcQuery, | ||
LexXrpcSubscription, | ||
} from './types' | ||
import { assertValidOneOf } from './util' | ||
@@ -15,2 +20,3 @@ | ||
if (!res.success) throw res.error | ||
return res.value | ||
} | ||
@@ -20,3 +26,3 @@ | ||
lexicons: Lexicons, | ||
def: LexXrpcProcedure | LexXrpcQuery, | ||
def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription, | ||
value: unknown, | ||
@@ -27,2 +33,3 @@ ) { | ||
if (!res.success) throw res.error | ||
return res.value | ||
} | ||
@@ -38,3 +45,3 @@ } | ||
// loop: all input schema definitions | ||
assertValidOneOf(lexicons, 'Input', def.input.schema, value, true) | ||
return assertValidOneOf(lexicons, 'Input', def.input.schema, value, true) | ||
} | ||
@@ -50,4 +57,21 @@ } | ||
// loop: all output schema definitions | ||
assertValidOneOf(lexicons, 'Output', def.output.schema, value, true) | ||
return assertValidOneOf(lexicons, 'Output', def.output.schema, value, true) | ||
} | ||
} | ||
export function assertValidXrpcMessage( | ||
lexicons: Lexicons, | ||
def: LexXrpcSubscription, | ||
value: unknown, | ||
) { | ||
if (def.message?.schema) { | ||
// loop: all output schema definitions | ||
return assertValidOneOf( | ||
lexicons, | ||
'Message', | ||
def.message.schema, | ||
value, | ||
true, | ||
) | ||
} | ||
} |
@@ -0,4 +1,4 @@ | ||
import { BlobRef } from '../blob-refs' | ||
import { Lexicons } from '../lexicons' | ||
import { LexUserType, ValidationResult, ValidationError } from '../types' | ||
import { isObj, hasProp } from '../types' | ||
@@ -11,48 +11,10 @@ export function blob( | ||
): ValidationResult { | ||
if (!isObj(value)) { | ||
// check | ||
if (!value || !(value instanceof BlobRef)) { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path} should be an object`), | ||
error: new ValidationError(`${path} should be a blob ref`), | ||
} | ||
} | ||
if (!hasProp(value, 'cid') || typeof value.cid !== 'string') { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path}/cid should be a string`), | ||
} | ||
} | ||
if (!hasProp(value, 'mimeType') || typeof value.mimeType !== 'string') { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path}/mimeType should be a string`), | ||
} | ||
} | ||
return { success: true } | ||
return { success: true, value } | ||
} | ||
export function image( | ||
lexicons: Lexicons, | ||
path: string, | ||
def: LexUserType, | ||
value: unknown, | ||
): ValidationResult { | ||
return blob(lexicons, path, def, value) | ||
} | ||
export function video( | ||
lexicons: Lexicons, | ||
path: string, | ||
def: LexUserType, | ||
value: unknown, | ||
): ValidationResult { | ||
return blob(lexicons, path, def, value) | ||
} | ||
export function audio( | ||
lexicons: Lexicons, | ||
path: string, | ||
def: LexUserType, | ||
value: unknown, | ||
): ValidationResult { | ||
return blob(lexicons, path, def, value) | ||
} |
@@ -23,4 +23,4 @@ import { Lexicons } from '../lexicons' | ||
return Primitives.boolean(lexicons, path, def, value) | ||
case 'number': | ||
return Primitives.number(lexicons, path, def, value) | ||
case 'float': | ||
return Primitives.float(lexicons, path, def, value) | ||
case 'integer': | ||
@@ -30,4 +30,6 @@ return Primitives.integer(lexicons, path, def, value) | ||
return Primitives.string(lexicons, path, def, value) | ||
case 'datetime': | ||
return Primitives.datetime(lexicons, path, def, value) | ||
case 'bytes': | ||
return Primitives.bytes(lexicons, path, def, value) | ||
case 'cid-link': | ||
return Primitives.cidLink(lexicons, path, def, value) | ||
case 'unknown': | ||
@@ -41,8 +43,2 @@ return Primitives.unknown(lexicons, path, def, value) | ||
return Blob.blob(lexicons, path, def, value) | ||
case 'image': | ||
return Blob.image(lexicons, path, def, value) | ||
case 'video': | ||
return Blob.video(lexicons, path, def, value) | ||
case 'audio': | ||
return Blob.audio(lexicons, path, def, value) | ||
default: | ||
@@ -59,7 +55,5 @@ return { | ||
path: string, | ||
def: LexUserType, | ||
def: LexArray, | ||
value: unknown, | ||
): ValidationResult { | ||
def = def as LexArray | ||
// type | ||
@@ -108,3 +102,3 @@ if (!Array.isArray(value)) { | ||
return { success: true } | ||
return { success: true, value } | ||
} | ||
@@ -128,31 +122,38 @@ | ||
// required | ||
if (Array.isArray(def.required)) { | ||
for (const key of def.required) { | ||
if (!(key in value)) { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path} must have the property "${key}"`), | ||
} | ||
} | ||
} | ||
} | ||
const requiredProps = new Set(def.required) | ||
const nullableProps = new Set(def.nullable) | ||
// properties | ||
let resultValue = value | ||
if (typeof def.properties === 'object') { | ||
for (const key in def.properties) { | ||
const propValue = value[key] | ||
if (typeof propValue === 'undefined') { | ||
continue // skip- if required, will have already failed | ||
if (value[key] === null && nullableProps.has(key)) { | ||
continue | ||
} | ||
const propDef = def.properties[key] | ||
const propPath = `${path}/${key}` | ||
const res = validateOneOf(lexicons, propPath, propDef, propValue) | ||
if (!res.success) { | ||
return res | ||
const validated = validateOneOf(lexicons, propPath, propDef, value[key]) | ||
const propValue = validated.success ? validated.value : value[key] | ||
const propIsUndefined = typeof propValue === 'undefined' | ||
// Return error for bad validation, giving required rule precedence | ||
if (propIsUndefined && requiredProps.has(key)) { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path} must have the property "${key}"`), | ||
} | ||
} else if (!propIsUndefined && !validated.success) { | ||
return validated | ||
} | ||
// Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value | ||
if (propValue !== value[key]) { | ||
if (resultValue === value) { | ||
// Lazy shallow clone | ||
resultValue = { ...value } | ||
} | ||
resultValue[key] = propValue | ||
} | ||
} | ||
} | ||
return { success: true } | ||
return { success: true, value: resultValue } | ||
} |
@@ -1,12 +0,14 @@ | ||
import { isValidISODateString } from 'iso-datestring-validator' | ||
import { utf8Len, graphemeLen } from '@atproto/common-web' | ||
import { CID } from 'multiformats/cid' | ||
import { Lexicons } from '../lexicons' | ||
import * as formats from './formats' | ||
import { | ||
LexUserType, | ||
LexBoolean, | ||
LexNumber, | ||
LexFloat, | ||
LexInteger, | ||
LexString, | ||
LexDatetime, | ||
ValidationResult, | ||
ValidationError, | ||
LexBytes, | ||
} from '../types' | ||
@@ -23,4 +25,4 @@ | ||
return boolean(lexicons, path, def, value) | ||
case 'number': | ||
return number(lexicons, path, def, value) | ||
case 'float': | ||
return float(lexicons, path, def, value) | ||
case 'integer': | ||
@@ -30,4 +32,6 @@ return integer(lexicons, path, def, value) | ||
return string(lexicons, path, def, value) | ||
case 'datetime': | ||
return datetime(lexicons, path, def, value) | ||
case 'bytes': | ||
return bytes(lexicons, path, def, value) | ||
case 'cid-link': | ||
return cidLink(lexicons, path, def, value) | ||
case 'unknown': | ||
@@ -53,5 +57,5 @@ return unknown(lexicons, path, def, value) | ||
const type = typeof value | ||
if (type == 'undefined') { | ||
if (type === 'undefined') { | ||
if (typeof def.default === 'boolean') { | ||
return { success: true } | ||
return { success: true, value: def.default } | ||
} | ||
@@ -79,6 +83,6 @@ return { | ||
return { success: true } | ||
return { success: true, value } | ||
} | ||
export function number( | ||
export function float( | ||
lexicons: Lexicons, | ||
@@ -89,9 +93,9 @@ path: string, | ||
): ValidationResult { | ||
def = def as LexNumber | ||
def = def as LexFloat | ||
// type | ||
const type = typeof value | ||
if (type == 'undefined') { | ||
if (type === 'undefined') { | ||
if (typeof def.default === 'number') { | ||
return { success: true } | ||
return { success: true, value: def.default } | ||
} | ||
@@ -155,3 +159,3 @@ return { | ||
return { success: true } | ||
return { success: true, value } | ||
} | ||
@@ -168,5 +172,7 @@ | ||
// run number validation | ||
const numRes = number(lexicons, path, def, value) | ||
const numRes = float(lexicons, path, def, value) | ||
if (!numRes.success) { | ||
return numRes | ||
} else { | ||
value = numRes.value | ||
} | ||
@@ -182,3 +188,3 @@ | ||
return { success: true } | ||
return { success: true, value } | ||
} | ||
@@ -195,6 +201,5 @@ | ||
// type | ||
const type = typeof value | ||
if (type == 'undefined') { | ||
if (typeof value === 'undefined') { | ||
if (typeof def.default === 'string') { | ||
return { success: true } | ||
return { success: true, value: def.default } | ||
} | ||
@@ -205,3 +210,3 @@ return { | ||
} | ||
} else if (type !== 'string') { | ||
} else if (typeof value !== 'string') { | ||
return { | ||
@@ -237,3 +242,3 @@ success: false, | ||
if (typeof def.maxLength === 'number') { | ||
if ((value as string).length > def.maxLength) { | ||
if (utf8Len(value) > def.maxLength) { | ||
return { | ||
@@ -250,3 +255,3 @@ success: false, | ||
if (typeof def.minLength === 'number') { | ||
if ((value as string).length < def.minLength) { | ||
if (utf8Len(value) < def.minLength) { | ||
return { | ||
@@ -261,6 +266,51 @@ success: false, | ||
return { success: true } | ||
// maxGraphemes | ||
if (typeof def.maxGraphemes === 'number') { | ||
if (graphemeLen(value) > def.maxGraphemes) { | ||
return { | ||
success: false, | ||
error: new ValidationError( | ||
`${path} must not be longer than ${def.maxGraphemes} graphemes`, | ||
), | ||
} | ||
} | ||
} | ||
// minGraphemes | ||
if (typeof def.minGraphemes === 'number') { | ||
if (graphemeLen(value) < def.minGraphemes) { | ||
return { | ||
success: false, | ||
error: new ValidationError( | ||
`${path} must not be shorter than ${def.minGraphemes} graphemes`, | ||
), | ||
} | ||
} | ||
} | ||
if (typeof def.format === 'string') { | ||
switch (def.format) { | ||
case 'datetime': | ||
return formats.datetime(path, value) | ||
case 'uri': | ||
return formats.uri(path, value) | ||
case 'at-uri': | ||
return formats.atUri(path, value) | ||
case 'did': | ||
return formats.did(path, value) | ||
case 'handle': | ||
return formats.handle(path, value) | ||
case 'at-identifier': | ||
return formats.atIdentifier(path, value) | ||
case 'nsid': | ||
return formats.nsid(path, value) | ||
case 'cid': | ||
return formats.cid(path, value) | ||
} | ||
} | ||
return { success: true, value } | ||
} | ||
export function datetime( | ||
export function bytes( | ||
lexicons: Lexicons, | ||
@@ -271,29 +321,54 @@ path: string, | ||
): ValidationResult { | ||
def = def as LexDatetime | ||
def = def as LexBytes | ||
// type | ||
const type = typeof value | ||
if (type !== 'string') { | ||
if (!value || !(value instanceof Uint8Array)) { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path} must be a string`), | ||
error: new ValidationError(`${path} must be a byte array`), | ||
} | ||
} | ||
// valid iso-8601 | ||
{ | ||
try { | ||
if (typeof value !== 'string' || !isValidISODateString(value)) { | ||
throw new ValidationError( | ||
`${path} must be an iso8601 formatted datetime`, | ||
) | ||
// maxLength | ||
if (typeof def.maxLength === 'number') { | ||
if (value.byteLength > def.maxLength) { | ||
return { | ||
success: false, | ||
error: new ValidationError( | ||
`${path} must not be larger than ${def.maxLength} bytes`, | ||
), | ||
} | ||
} catch { | ||
throw new ValidationError(`${path} must be an iso8601 formatted datetime`) | ||
} | ||
} | ||
return { success: true } | ||
// minLength | ||
if (typeof def.minLength === 'number') { | ||
if (value.byteLength < def.minLength) { | ||
return { | ||
success: false, | ||
error: new ValidationError( | ||
`${path} must not be smaller than ${def.minLength} bytes`, | ||
), | ||
} | ||
} | ||
} | ||
return { success: true, value } | ||
} | ||
export function cidLink( | ||
lexicons: Lexicons, | ||
path: string, | ||
def: LexUserType, | ||
value: unknown, | ||
): ValidationResult { | ||
if (CID.asCID(value) === null) { | ||
return { | ||
success: false, | ||
error: new ValidationError(`${path} must be a CID`), | ||
} | ||
} | ||
return { success: true, value } | ||
} | ||
export function unknown( | ||
@@ -313,3 +388,3 @@ lexicons: Lexicons, | ||
return { success: true } | ||
return { success: true, value } | ||
} |
@@ -5,2 +5,3 @@ import { Lexicons } from '../lexicons' | ||
import * as PrimitiveValidators from './primitives' | ||
import { array } from './complex' | ||
@@ -11,16 +12,22 @@ export function params( | ||
def: LexXrpcParameters, | ||
value: unknown, | ||
val: unknown, | ||
): ValidationResult { | ||
def = def as LexXrpcParameters | ||
// type | ||
if (!value || typeof value !== 'object') { | ||
// in this case, we just fall back to an object | ||
value = {} | ||
} | ||
const value = val && typeof val === 'object' ? val : {} | ||
// required | ||
if (Array.isArray(def.required)) { | ||
for (const key of def.required) { | ||
if (!(key in (value as Record<string, unknown>))) { | ||
const requiredProps = new Set(def.required ?? []) | ||
// properties | ||
let resultValue = value | ||
if (typeof def.properties === 'object') { | ||
for (const key in def.properties) { | ||
const propDef = def.properties[key] | ||
const validated = | ||
propDef.type === 'array' | ||
? array(lexicons, key, propDef, value[key]) | ||
: PrimitiveValidators.validate(lexicons, key, propDef, value[key]) | ||
const propValue = validated.success ? validated.value : value[key] | ||
const propIsUndefined = typeof propValue === 'undefined' | ||
// Return error for bad validation, giving required rule precedence | ||
if (propIsUndefined && requiredProps.has(key)) { | ||
return { | ||
@@ -30,24 +37,17 @@ success: false, | ||
} | ||
} else if (!propIsUndefined && !validated.success) { | ||
return validated | ||
} | ||
// Adjust value based on e.g. applied defaults, cloning shallowly if there was a changed value | ||
if (propValue !== value[key]) { | ||
if (resultValue === value) { | ||
// Lazy shallow clone | ||
resultValue = { ...value } | ||
} | ||
resultValue[key] = propValue | ||
} | ||
} | ||
} | ||
// properties | ||
for (const key in def.properties) { | ||
if (typeof (value as Record<string, unknown>)[key] === 'undefined') { | ||
continue // skip- if required, will have already failed | ||
} | ||
const paramDef = def.properties[key] | ||
const res = PrimitiveValidators.validate( | ||
lexicons, | ||
key, | ||
paramDef, | ||
(value as Record<string, unknown>)[key], | ||
) | ||
if (!res.success) { | ||
return res | ||
} | ||
} | ||
return { success: true } | ||
return { success: true, value: resultValue } | ||
} |
@@ -16,6 +16,7 @@ export default [ | ||
'boolean', | ||
'number', | ||
'float', | ||
'integer', | ||
'string', | ||
'datetime', | ||
'bytes', | ||
'cidLink', | ||
], | ||
@@ -26,6 +27,7 @@ properties: { | ||
boolean: { type: 'boolean' }, | ||
number: { type: 'number' }, | ||
float: { type: 'float' }, | ||
integer: { type: 'integer' }, | ||
string: { type: 'string' }, | ||
datetime: { type: 'datetime' }, | ||
bytes: { type: 'bytes' }, | ||
cidLink: { type: 'cid-link' }, | ||
}, | ||
@@ -36,3 +38,3 @@ }, | ||
type: 'object', | ||
required: ['object', 'array', 'boolean', 'number', 'integer', 'string'], | ||
required: ['object', 'array', 'boolean', 'float', 'integer', 'string'], | ||
properties: { | ||
@@ -42,3 +44,3 @@ object: { type: 'ref', ref: '#subobject' }, | ||
boolean: { type: 'boolean' }, | ||
number: { type: 'number' }, | ||
float: { type: 'float' }, | ||
integer: { type: 'integer' }, | ||
@@ -66,8 +68,10 @@ string: { type: 'string' }, | ||
type: 'params', | ||
required: ['boolean', 'number', 'integer'], | ||
required: ['boolean', 'float', 'integer'], | ||
properties: { | ||
boolean: { type: 'boolean' }, | ||
number: { type: 'number' }, | ||
float: { type: 'float' }, | ||
integer: { type: 'integer' }, | ||
string: { type: 'string' }, | ||
array: { type: 'array', items: { type: 'string' } }, | ||
def: { type: 'integer', default: 0 }, | ||
}, | ||
@@ -91,8 +95,9 @@ }, | ||
type: 'params', | ||
required: ['boolean', 'number', 'integer'], | ||
required: ['boolean', 'float', 'integer'], | ||
properties: { | ||
boolean: { type: 'boolean' }, | ||
number: { type: 'number' }, | ||
float: { type: 'float' }, | ||
integer: { type: 'integer' }, | ||
string: { type: 'string' }, | ||
array: { type: 'array', items: { type: 'string' } }, | ||
}, | ||
@@ -123,3 +128,3 @@ }, | ||
boolean: { type: 'boolean' }, | ||
number: { type: 'number' }, | ||
float: { type: 'float' }, | ||
integer: { type: 'integer' }, | ||
@@ -134,2 +139,31 @@ string: { type: 'string' }, | ||
lexicon: 1, | ||
id: 'com.example.default', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
required: ['boolean'], | ||
properties: { | ||
boolean: { type: 'boolean', default: false }, | ||
float: { type: 'float', default: 0 }, | ||
integer: { type: 'integer', default: 0 }, | ||
string: { type: 'string', default: '' }, | ||
object: { type: 'ref', ref: '#object' }, | ||
}, | ||
}, | ||
}, | ||
object: { | ||
type: 'object', | ||
properties: { | ||
boolean: { type: 'boolean', default: true }, | ||
float: { type: 'float', default: 1.5 }, | ||
integer: { type: 'integer', default: 1 }, | ||
string: { type: 'string', default: 'x' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.union', | ||
@@ -197,3 +231,3 @@ defs: { | ||
maxLength: 4, | ||
items: { type: 'number' }, | ||
items: { type: 'float' }, | ||
}, | ||
@@ -225,3 +259,3 @@ }, | ||
lexicon: 1, | ||
id: 'com.example.numberRange', | ||
id: 'com.example.floatRange', | ||
defs: { | ||
@@ -233,4 +267,4 @@ main: { | ||
properties: { | ||
number: { | ||
type: 'number', | ||
float: { | ||
type: 'float', | ||
minimum: 2, | ||
@@ -246,3 +280,3 @@ maximum: 4, | ||
lexicon: 1, | ||
id: 'com.example.numberEnum', | ||
id: 'com.example.floatEnum', | ||
defs: { | ||
@@ -254,4 +288,4 @@ main: { | ||
properties: { | ||
number: { | ||
type: 'number', | ||
float: { | ||
type: 'float', | ||
enum: [1, 1.5, 2], | ||
@@ -266,3 +300,3 @@ }, | ||
lexicon: 1, | ||
id: 'com.example.numberConst', | ||
id: 'com.example.floatConst', | ||
defs: { | ||
@@ -274,4 +308,4 @@ main: { | ||
properties: { | ||
number: { | ||
type: 'number', | ||
float: { | ||
type: 'float', | ||
const: 0, | ||
@@ -360,2 +394,21 @@ }, | ||
lexicon: 1, | ||
id: 'com.example.stringLengthGrapheme', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
string: { | ||
type: 'string', | ||
minGraphemes: 2, | ||
maxGraphemes: 4, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.stringEnum', | ||
@@ -404,4 +457,126 @@ defs: { | ||
properties: { | ||
datetime: { | ||
type: 'datetime', | ||
datetime: { type: 'string', format: 'datetime' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.uri', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
uri: { type: 'string', format: 'uri' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.atUri', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
atUri: { type: 'string', format: 'at-uri' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.did', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
did: { type: 'string', format: 'did' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.handle', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
handle: { type: 'string', format: 'handle' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.atIdentifier', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
atIdentifier: { type: 'string', format: 'at-identifier' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.nsid', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
nsid: { type: 'string', format: 'nsid' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.cid', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
cid: { type: 'string', format: 'cid' }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
lexicon: 1, | ||
id: 'com.example.byteLength', | ||
defs: { | ||
main: { | ||
type: 'record', | ||
record: { | ||
type: 'object', | ||
properties: { | ||
bytes: { | ||
type: 'bytes', | ||
minLength: 2, | ||
maxLength: 4, | ||
}, | ||
@@ -408,0 +583,0 @@ }, |
@@ -0,1 +1,2 @@ | ||
import { CID } from 'multiformats/cid' | ||
import { Lexicons } from '../src/index' | ||
@@ -43,3 +44,3 @@ import LexiconDocs from './_scaffolds/lexicons' | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -50,6 +51,13 @@ string: 'string', | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
atUri: 'at://did:web:example.com/com.example.test/self', | ||
did: 'did:web:example.com', | ||
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
bytes: new Uint8Array([0, 1, 2, 3]), | ||
cidLink: CID.parse( | ||
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
), | ||
}) | ||
@@ -61,2 +69,3 @@ expect(res.success).toBe(true) | ||
expect(res.success).toBe(false) | ||
if (res.success) throw new Error('Asserted') | ||
expect(res.error?.message).toBe('Record must have the property "object"') | ||
@@ -71,3 +80,3 @@ } | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -81,2 +90,3 @@ string: 'string', | ||
expect(res.success).toBe(false) | ||
if (res.success) throw new Error('Asserted') | ||
expect(res.error?.message).toBe('Object must have the property "object"') | ||
@@ -90,20 +100,25 @@ } | ||
it('Passes valid schemas', () => { | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
const passingSink = { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
}) | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
bytes: new Uint8Array([0, 1, 2, 3]), | ||
cidLink: CID.parse( | ||
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
), | ||
} | ||
it('Passes valid schemas', () => { | ||
lex.assertValidRecord('com.example.kitchenSink', passingSink) | ||
}) | ||
@@ -138,8 +153,21 @@ | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
atUri: 'at://did:web:example.com/com.example.test/self', | ||
did: 'did:web:example.com', | ||
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
bytes: new Uint8Array([0, 1, 2, 3]), | ||
cidLink: CID.parse( | ||
'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
), | ||
}), | ||
).toThrow('Record must have the property "object"') | ||
expect(() => | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
...passingSink, | ||
object: undefined, | ||
}), | ||
).toThrow('Record must have the property "object"') | ||
}) | ||
@@ -150,17 +178,7 @@ | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
...passingSink, | ||
object: { | ||
...passingSink.object, | ||
object: { boolean: '1234' }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
}), | ||
@@ -170,10 +188,4 @@ ).toThrow('Record/object/object/boolean must be a boolean') | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
...passingSink, | ||
object: true, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
}), | ||
@@ -183,17 +195,4 @@ ).toThrow('Record/object must be an object') | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
...passingSink, | ||
array: 1234, | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
}), | ||
@@ -203,36 +202,10 @@ ).toThrow('Record/array must be an array') | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 'string', | ||
integer: 123, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
...passingSink, | ||
float: 'string', | ||
}), | ||
).toThrow('Record/number must be a number') | ||
).toThrow('Record/float must be a number') | ||
expect(() => | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
...passingSink, | ||
integer: true, | ||
string: 'string', | ||
datetime: new Date().toISOString(), | ||
}), | ||
@@ -242,17 +215,4 @@ ).toThrow('Record/integer must be a number') | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
...passingSink, | ||
string: {}, | ||
datetime: new Date().toISOString(), | ||
}), | ||
@@ -262,19 +222,12 @@ ).toThrow('Record/string must be a string') | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
$type: 'com.example.kitchenSink', | ||
object: { | ||
object: { boolean: true }, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
}, | ||
array: ['one', 'two'], | ||
boolean: true, | ||
number: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
datetime: 1234, | ||
...passingSink, | ||
bytes: 1234, | ||
}), | ||
).toThrow('Record/datetime must be a string') | ||
).toThrow('Record/bytes must be a byte array') | ||
expect(() => | ||
lex.assertValidRecord('com.example.kitchenSink', { | ||
...passingSink, | ||
cidLink: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
}), | ||
).toThrow('Record/cidLink must be a CID') | ||
}) | ||
@@ -288,2 +241,23 @@ | ||
it('Handles default properties correctly', () => { | ||
const result = lex.assertValidRecord('com.example.default', { | ||
$type: 'com.example.default', | ||
object: {}, | ||
}) | ||
expect(result).toEqual({ | ||
$type: 'com.example.default', | ||
boolean: false, | ||
integer: 0, | ||
float: 0, | ||
string: '', | ||
object: { | ||
boolean: true, | ||
integer: 1, | ||
float: 1.5, | ||
string: 'x', | ||
}, | ||
}) | ||
expect(result).not.toHaveProperty('datetime') | ||
}) | ||
it('Handles unions correctly', () => { | ||
@@ -297,3 +271,3 @@ lex.assertValidRecord('com.example.union', { | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -373,2 +347,17 @@ string: 'string', | ||
it('Applies array item constraints', () => { | ||
expect(() => | ||
lex.assertValidRecord('com.example.arrayLength', { | ||
$type: 'com.example.arrayLength', | ||
array: [1, '2', 3], | ||
}), | ||
).toThrow('Record/array/1 must be a number') | ||
expect(() => | ||
lex.assertValidRecord('com.example.arrayLength', { | ||
$type: 'com.example.arrayLength', | ||
array: [1, undefined, 3], | ||
}), | ||
).toThrow('Record/array/1 must be a number') | ||
}) | ||
it('Applies boolean const constraint', () => { | ||
@@ -387,45 +376,45 @@ lex.assertValidRecord('com.example.boolConst', { | ||
it('Applies number range constraint', () => { | ||
lex.assertValidRecord('com.example.numberRange', { | ||
$type: 'com.example.numberRange', | ||
number: 2.5, | ||
it('Applies float range constraint', () => { | ||
lex.assertValidRecord('com.example.floatRange', { | ||
$type: 'com.example.floatRange', | ||
float: 2.5, | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.numberRange', { | ||
$type: 'com.example.numberRange', | ||
number: 1, | ||
lex.assertValidRecord('com.example.floatRange', { | ||
$type: 'com.example.floatRange', | ||
float: 1, | ||
}), | ||
).toThrow('Record/number can not be less than 2') | ||
).toThrow('Record/float can not be less than 2') | ||
expect(() => | ||
lex.assertValidRecord('com.example.numberRange', { | ||
$type: 'com.example.numberRange', | ||
number: 5, | ||
lex.assertValidRecord('com.example.floatRange', { | ||
$type: 'com.example.floatRange', | ||
float: 5, | ||
}), | ||
).toThrow('Record/number can not be greater than 4') | ||
).toThrow('Record/float can not be greater than 4') | ||
}) | ||
it('Applies number enum constraint', () => { | ||
lex.assertValidRecord('com.example.numberEnum', { | ||
$type: 'com.example.numberEnum', | ||
number: 1.5, | ||
it('Applies float enum constraint', () => { | ||
lex.assertValidRecord('com.example.floatEnum', { | ||
$type: 'com.example.floatEnum', | ||
float: 1.5, | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.numberEnum', { | ||
$type: 'com.example.numberEnum', | ||
number: 0, | ||
lex.assertValidRecord('com.example.floatEnum', { | ||
$type: 'com.example.floatEnum', | ||
float: 0, | ||
}), | ||
).toThrow('Record/number must be one of (1|1.5|2)') | ||
).toThrow('Record/float must be one of (1|1.5|2)') | ||
}) | ||
it('Applies number const constraint', () => { | ||
lex.assertValidRecord('com.example.numberConst', { | ||
$type: 'com.example.numberConst', | ||
number: 0, | ||
it('Applies float const constraint', () => { | ||
lex.assertValidRecord('com.example.floatConst', { | ||
$type: 'com.example.floatConst', | ||
float: 0, | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.numberConst', { | ||
$type: 'com.example.numberConst', | ||
number: 1, | ||
lex.assertValidRecord('com.example.floatConst', { | ||
$type: 'com.example.floatConst', | ||
float: 1, | ||
}), | ||
).toThrow('Record/number must be 0') | ||
).toThrow('Record/float must be 0') | ||
}) | ||
@@ -504,4 +493,29 @@ | ||
).toThrow('Record/string must not be longer than 4 characters') | ||
expect(() => | ||
lex.assertValidRecord('com.example.stringLength', { | ||
$type: 'com.example.stringLength', | ||
string: '๐จโ๐ฉโ๐งโ๐ง', | ||
}), | ||
).toThrow('Record/string must not be longer than 4 characters') | ||
}) | ||
it('Applies grapheme string length constraint', () => { | ||
lex.assertValidRecord('com.example.stringLengthGrapheme', { | ||
$type: 'com.example.stringLengthGrapheme', | ||
string: '12๐จโ๐ฉโ๐งโ๐ง', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.stringLengthGrapheme', { | ||
$type: 'com.example.stringLengthGrapheme', | ||
string: '๐จโ๐ฉโ๐งโ๐ง', | ||
}), | ||
).toThrow('Record/string must not be shorter than 2 graphemes') | ||
expect(() => | ||
lex.assertValidRecord('com.example.stringLengthGrapheme', { | ||
$type: 'com.example.stringLengthGrapheme', | ||
string: '12345', | ||
}), | ||
).toThrow('Record/string must not be longer than 4 graphemes') | ||
}) | ||
it('Applies string enum constraint', () => { | ||
@@ -558,2 +572,164 @@ lex.assertValidRecord('com.example.stringEnum', { | ||
}) | ||
it('Applies uri formatting constraint', () => { | ||
for (const uri of [ | ||
'https://example.com', | ||
'https://example.com/with/path', | ||
'https://example.com/with/path?and=query', | ||
'at://bsky.social', | ||
'did:example:test', | ||
]) { | ||
lex.assertValidRecord('com.example.uri', { | ||
$type: 'com.example.uri', | ||
uri, | ||
}) | ||
} | ||
expect(() => | ||
lex.assertValidRecord('com.example.uri', { | ||
$type: 'com.example.uri', | ||
uri: 'not a uri', | ||
}), | ||
).toThrow('Record/uri must be a uri') | ||
}) | ||
it('Applies at-uri formatting constraint', () => { | ||
lex.assertValidRecord('com.example.atUri', { | ||
$type: 'com.example.atUri', | ||
atUri: 'at://did:web:example.com/com.example.test/self', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.atUri', { | ||
$type: 'com.example.atUri', | ||
atUri: 'http://not-atproto.com', | ||
}), | ||
).toThrow('Record/atUri must be a valid at-uri') | ||
}) | ||
it('Applies did formatting constraint', () => { | ||
lex.assertValidRecord('com.example.did', { | ||
$type: 'com.example.did', | ||
did: 'did:web:example.com', | ||
}) | ||
lex.assertValidRecord('com.example.did', { | ||
$type: 'com.example.did', | ||
did: 'did:plc:12345678abcdefghijklmnop', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.did', { | ||
$type: 'com.example.did', | ||
did: 'bad did', | ||
}), | ||
).toThrow('Record/did must be a valid did') | ||
expect(() => | ||
lex.assertValidRecord('com.example.did', { | ||
$type: 'com.example.did', | ||
did: 'did:short', | ||
}), | ||
).toThrow('Record/did must be a valid did') | ||
}) | ||
it('Applies handle formatting constraint', () => { | ||
lex.assertValidRecord('com.example.handle', { | ||
$type: 'com.example.handle', | ||
handle: 'test.bsky.social', | ||
}) | ||
lex.assertValidRecord('com.example.handle', { | ||
$type: 'com.example.handle', | ||
handle: 'bsky.test', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.handle', { | ||
$type: 'com.example.handle', | ||
handle: 'bad handle', | ||
}), | ||
).toThrow('Record/handle must be a valid handle') | ||
expect(() => | ||
lex.assertValidRecord('com.example.handle', { | ||
$type: 'com.example.handle', | ||
handle: '-bad-.test', | ||
}), | ||
).toThrow('Record/handle must be a valid handle') | ||
}) | ||
it('Applies at-identifier formatting constraint', () => { | ||
lex.assertValidRecord('com.example.atIdentifier', { | ||
$type: 'com.example.atIdentifier', | ||
atIdentifier: 'bsky.test', | ||
}) | ||
lex.assertValidRecord('com.example.atIdentifier', { | ||
$type: 'com.example.atIdentifier', | ||
atIdentifier: 'did:plc:12345678abcdefghijklmnop', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.atIdentifier', { | ||
$type: 'com.example.atIdentifier', | ||
atIdentifier: 'bad id', | ||
}), | ||
).toThrow('Record/atIdentifier must be a valid did or a handle') | ||
expect(() => | ||
lex.assertValidRecord('com.example.atIdentifier', { | ||
$type: 'com.example.atIdentifier', | ||
atIdentifier: '-bad-.test', | ||
}), | ||
).toThrow('Record/atIdentifier must be a valid did or a handle') | ||
}) | ||
it('Applies nsid formatting constraint', () => { | ||
lex.assertValidRecord('com.example.nsid', { | ||
$type: 'com.example.nsid', | ||
nsid: 'com.atproto.test', | ||
}) | ||
lex.assertValidRecord('com.example.nsid', { | ||
$type: 'com.example.nsid', | ||
nsid: 'app.bsky.nested.test', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.nsid', { | ||
$type: 'com.example.nsid', | ||
nsid: 'bad nsid', | ||
}), | ||
).toThrow('Record/nsid must be a valid nsid') | ||
expect(() => | ||
lex.assertValidRecord('com.example.nsid', { | ||
$type: 'com.example.nsid', | ||
nsid: 'com.bad-.foo', | ||
}), | ||
).toThrow('Record/nsid must be a valid nsid') | ||
}) | ||
it('Applies cid formatting constraint', () => { | ||
lex.assertValidRecord('com.example.cid', { | ||
$type: 'com.example.cid', | ||
cid: 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a', | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.cid', { | ||
$type: 'com.example.cid', | ||
cid: 'abapsdofiuwrpoiasdfuaspdfoiu', | ||
}), | ||
).toThrow('Record/cid must be a cid string') | ||
}) | ||
it('Applies bytes length constraints', () => { | ||
lex.assertValidRecord('com.example.byteLength', { | ||
$type: 'com.example.byteLength', | ||
bytes: new Uint8Array([1, 2, 3]), | ||
}) | ||
expect(() => | ||
lex.assertValidRecord('com.example.byteLength', { | ||
$type: 'com.example.byteLength', | ||
bytes: new Uint8Array([1]), | ||
}), | ||
).toThrow('Record/bytes must not be smaller than 2 bytes') | ||
expect(() => | ||
lex.assertValidRecord('com.example.byteLength', { | ||
$type: 'com.example.byteLength', | ||
bytes: new Uint8Array([1, 2, 3, 4, 5]), | ||
}), | ||
).toThrow('Record/bytes must not be larger than 4 bytes') | ||
}) | ||
}) | ||
@@ -565,14 +741,33 @@ | ||
it('Passes valid parameters', () => { | ||
lex.assertValidXrpcParams('com.example.query', { | ||
const queryResult = lex.assertValidXrpcParams('com.example.query', { | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
array: ['x', 'y'], | ||
}) | ||
lex.assertValidXrpcParams('com.example.procedure', { | ||
expect(queryResult).toEqual({ | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
array: ['x', 'y'], | ||
def: 0, | ||
}) | ||
const paramResult = lex.assertValidXrpcParams('com.example.procedure', { | ||
boolean: true, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
array: ['x', 'y'], | ||
def: 1, | ||
}) | ||
expect(paramResult).toEqual({ | ||
boolean: true, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
array: ['x', 'y'], | ||
def: 1, | ||
}) | ||
}) | ||
@@ -583,3 +778,3 @@ | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -590,5 +785,12 @@ }) | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
}), | ||
).toThrow('Params must have the property "integer"') | ||
expect(() => | ||
lex.assertValidXrpcParams('com.example.query', { | ||
boolean: true, | ||
float: 123.45, | ||
integer: undefined, | ||
}), | ||
).toThrow('Params must have the property "integer"') | ||
}) | ||
@@ -600,3 +802,3 @@ | ||
boolean: 'string', | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -609,7 +811,16 @@ string: 'string', | ||
boolean: true, | ||
number: true, | ||
float: true, | ||
integer: 123, | ||
string: 'string', | ||
}), | ||
).toThrow('number must be a number') | ||
).toThrow('float must be a number') | ||
expect(() => | ||
lex.assertValidXrpcParams('com.example.query', { | ||
boolean: true, | ||
float: 123.45, | ||
integer: 123, | ||
string: 'string', | ||
array: 'x', | ||
}), | ||
).toThrow('array must be an array') | ||
}) | ||
@@ -626,3 +837,3 @@ }) | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -640,3 +851,3 @@ string: 'string', | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -660,3 +871,3 @@ string: 'string', | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -669,3 +880,3 @@ string: 'string', | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -683,3 +894,3 @@ string: 'string', | ||
boolean: true, | ||
number: 123.45, | ||
float: 123.45, | ||
integer: 123, | ||
@@ -686,0 +897,0 @@ string: 'string', |
@@ -10,4 +10,6 @@ { | ||
"references": [ | ||
{ "path": "../nsid/tsconfig.build.json" } | ||
{ "path": "../common/tsconfig.build.json" }, | ||
{ "path": "../nsid/tsconfig.build.json" }, | ||
{ "path": "../uri/tsconfig.build.json" } | ||
] | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 3 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2305566
36
48476
7
4
+ Added@atproto/common-web@*
+ Added@atproto/identifier@*
+ Added@atproto/uri@*
+ Addedmultiformats@^9.6.4
+ Added@atproto/common-web@0.3.1(transitive)
+ Added@atproto/identifier@0.2.1(transitive)
+ Added@atproto/uri@0.1.1(transitive)