Skip to content

Commit

Permalink
lex-cli improvements (bluesky-social#2911)
Browse files Browse the repository at this point in the history
* Retain type of `schemas` using definition type instead of obscuring into a `LexiconDoc[]`

* Improve validation performances by using discriminated unions where possible

* Export the generated lexicons `schemas` definitions

* optimization

* changeset

* tidy
  • Loading branch information
matthieusieben authored Nov 8, 2024
1 parent c6a9678 commit bac9be2
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-doors-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/syntax": patch
---

Improve performances of did validation
5 changes: 5 additions & 0 deletions .changeset/silly-poems-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/lex-cli": patch
---

Retain type of `schemas` using definition type instead of obscuring into a `LexiconDoc[]`
5 changes: 5 additions & 0 deletions .changeset/six-cats-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/api": patch
---

Export the generated lexicons `schemas` definitions
5 changes: 5 additions & 0 deletions .changeset/stupid-impalas-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@atproto/lexicon": patch
---

Improve validation performances by using discriminated unions where possible
5 changes: 3 additions & 2 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13327,8 +13327,9 @@ export const schemaDict = {
},
},
},
}
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
} as const satisfies Record<string, LexiconDoc>

export const schemas = Object.values(schemaDict)
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './types'
export * from './const'
export * from './util'
export * from './client'
export { schemas } from './client/lexicons'
export * from './rich-text/rich-text'
export * from './rich-text/sanitization'
export * from './rich-text/unicode'
Expand Down
5 changes: 3 additions & 2 deletions packages/bsky/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10566,8 +10566,9 @@ export const schemaDict = {
},
},
},
}
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
} as const satisfies Record<string, LexiconDoc>

export const schemas = Object.values(schemaDict)
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
Expand Down
30 changes: 15 additions & 15 deletions packages/lex-cli/src/codegen/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,37 +43,37 @@ export const lexiconsTs = (project, lexicons: LexiconDoc[]) =>
})
.addNamedImports([{ name: 'LexiconDoc' }, { name: 'Lexicons' }])

//= export const schemaDict: Record<string, LexiconDoc> = {...}
//= export const schemaDict = {...} as const satisfies Record<string, LexiconDoc>
file.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: 'schemaDict',
initializer: JSON.stringify(
lexicons.reduce(
(acc, cur) => ({
...acc,
[nsidToEnum(cur.id)]: cur,
}),
{},
),
null,
2,
),
initializer:
JSON.stringify(
lexicons.reduce(
(acc, cur) => ({
...acc,
[nsidToEnum(cur.id)]: cur,
}),
{},
),
null,
2,
) + ' as const satisfies Record<string, LexiconDoc>',
},
],
})

//= export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
//= export const schemas = Object.values(schemaDict)
file.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: 'schemas',
type: 'LexiconDoc[]',
initializer: 'Object.values(schemaDict) as LexiconDoc[]',
initializer: 'Object.values(schemaDict)',
},
],
})
Expand Down
55 changes: 52 additions & 3 deletions packages/lexicon/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,21 @@ export const lexArray = z
.object({
type: z.literal('array'),
description: z.string().optional(),
items: z.union([lexPrimitive, lexIpldType, lexBlob, lexRefVariant]),
items: z.discriminatedUnion('type', [
// lexPrimitive
lexBoolean,
lexInteger,
lexString,
lexUnknown,
// lexIpldType
lexBytes,
lexCidLink,
// lexRefVariant
lexRef,
lexRefUnion,
// other
lexBlob,
]),
minLength: z.number().int().optional(),
maxLength: z.number().int().optional(),
})
Expand Down Expand Up @@ -176,7 +190,23 @@ export const lexObject = z
required: z.string().array().optional(),
nullable: z.string().array().optional(),
properties: z.record(
z.union([lexRefVariant, lexIpldType, lexArray, lexBlob, lexPrimitive]),
z.discriminatedUnion('type', [
lexArray,

// lexPrimitive
lexBoolean,
lexInteger,
lexString,
lexUnknown,
// lexIpldType
lexBytes,
lexCidLink,
// lexRefVariant
lexRef,
lexRefUnion,
// other
lexBlob,
]),
),
})
.strict()
Expand All @@ -191,7 +221,17 @@ export const lexXrpcParameters = z
type: z.literal('params'),
description: z.string().optional(),
required: z.string().array().optional(),
properties: z.record(z.union([lexPrimitive, lexPrimitiveArray])),
properties: z.record(
z.discriminatedUnion('type', [
lexPrimitiveArray,

// lexPrimitive
lexBoolean,
lexInteger,
lexString,
lexUnknown,
]),
),
})
.strict()
.superRefine(requiredPropertiesRefinement)
Expand All @@ -201,6 +241,7 @@ export const lexXrpcBody = z
.object({
description: z.string().optional(),
encoding: z.string(),
// @NOTE using discriminatedUnion with a refined schema requires zod >= 4
schema: z.union([lexRefVariant, lexObject]).optional(),
})
.strict()
Expand All @@ -209,6 +250,7 @@ export type LexXrpcBody = z.infer<typeof lexXrpcBody>
export const lexXrpcSubscriptionMessage = z
.object({
description: z.string().optional(),
// @NOTE using discriminatedUnion with a refined schema requires zod >= 4
schema: z.union([lexRefVariant, lexObject]).optional(),
})
.strict()
Expand Down Expand Up @@ -353,6 +395,13 @@ export const lexUserType = z.custom<
}
}

if (typeof val['type'] !== 'string') {
return {
message: 'Type property must be a string',
fatal: true,
}
}

return {
message: `Invalid type: ${val['type']} must be one of: record, query, procedure, subscription, blob, array, token, object, boolean, integer, string, bytes, cid-link, unknown`,
fatal: true,
Expand Down
25 changes: 15 additions & 10 deletions packages/lexicon/src/validators/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function atUri(path: string, value: string): ValidationResult {
error: new ValidationError(`${path} must be a valid at-uri`),
}
}

return { success: true, value }
}

Expand All @@ -59,6 +60,7 @@ export function did(path: string, value: string): ValidationResult {
error: new ValidationError(`${path} must be a valid did`),
}
}

return { success: true, value }
}

Expand All @@ -71,21 +73,24 @@ export function handle(path: string, value: string): ValidationResult {
error: new ValidationError(`${path} must be a valid handle`),
}
}

return { success: true, value }
}

export function atIdentifier(path: string, value: string): ValidationResult {
const isDid = did(path, value)
if (!isDid.success) {
const isHandle = handle(path, value)
if (!isHandle.success) {
return {
success: false,
error: new ValidationError(`${path} must be a valid did or a handle`),
}
}
// We can discriminate based on the "did:" prefix
if (value.startsWith('did:')) {
const didResult = did(path, value)
if (didResult.success) return didResult
} else {
const handleResult = handle(path, value)
if (handleResult.success) return handleResult
}

return {
success: false,
error: new ValidationError(`${path} must be a valid did or a handle`),
}
return { success: true, value }
}

export function nsid(path: string, value: string): ValidationResult {
Expand Down
5 changes: 3 additions & 2 deletions packages/ozone/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13327,8 +13327,9 @@ export const schemaDict = {
},
},
},
}
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
} as const satisfies Record<string, LexiconDoc>

export const schemas = Object.values(schemaDict)
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
Expand Down
5 changes: 3 additions & 2 deletions packages/pds/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13327,8 +13327,9 @@ export const schemaDict = {
},
},
},
}
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
} as const satisfies Record<string, LexiconDoc>

export const schemas = Object.values(schemaDict)
export const lexicons: Lexicons = new Lexicons(schemas)
export const ids = {
ComAtprotoAdminDefs: 'com.atproto.admin.defs',
Expand Down
14 changes: 7 additions & 7 deletions packages/syntax/src/did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@
// - hard length limit of 8KBytes
// - not going to validate "percent encoding" here
export const ensureValidDid = (did: string): void => {
if (!did.startsWith('did:')) {
throw new InvalidDidError('DID requires "did:" prefix')
}

// check that all chars are boring ASCII
if (!/^[a-zA-Z0-9._:%-]*$/.test(did)) {
throw new InvalidDidError(
'Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)',
)
}

const parts = did.split(':')
if (parts.length < 3) {
const { length, 1: method } = did.split(':')
if (length < 3) {
throw new InvalidDidError(
'DID requires prefix, method, and method-specific content',
)
}

if (parts[0] !== 'did') {
throw new InvalidDidError('DID requires "did:" prefix')
}

if (!/^[a-z]+$/.test(parts[1])) {
if (!/^[a-z]+$/.test(method)) {
throw new InvalidDidError('DID method must be lower-case letters')
}

Expand Down

0 comments on commit bac9be2

Please sign in to comment.