Suite of AT Protocol TypeScript libraries built on web standards
1import {
2 assertStringFormat,
3 type InferStringFormat,
4 type StringFormat,
5} from "../core/string-format.ts";
6import {
7 Schema,
8 type ValidationResult,
9 type ValidatorContext,
10} from "../validation.ts";
11import { graphemeLen, utf8Len } from "../data/strings.ts";
12import { asCid } from "../data/cid.ts";
13import { TokenSchema } from "./token.ts";
14
15export type StringSchemaOptions = {
16 default?: string;
17 format?: StringFormat;
18 minLength?: number;
19 maxLength?: number;
20 minGraphemes?: number;
21 maxGraphemes?: number;
22};
23
24export type StringSchemaOutput<Options> = Options extends
25 { format: infer F extends StringFormat } ? InferStringFormat<F>
26 : string;
27
28export class StringSchema<
29 const Options extends StringSchemaOptions,
30> extends Schema<StringSchemaOutput<Options>> {
31 constructor(readonly options: Options) {
32 super();
33 }
34
35 validateInContext(
36 input: unknown = this.options.default,
37 ctx: ValidatorContext,
38 ): ValidationResult<StringSchemaOutput<Options>> {
39 const { options } = this;
40
41 const str = coerceToString(input);
42 if (str == null) {
43 return ctx.issueInvalidType(input, "string");
44 }
45
46 let lazyUtf8Len: number;
47
48 const { minLength } = options;
49 if (minLength != null) {
50 if ((lazyUtf8Len ??= utf8Len(str)) < minLength) {
51 return ctx.issueTooSmall(str, "string", minLength, lazyUtf8Len);
52 }
53 }
54
55 const { maxLength } = options;
56 if (maxLength != null) {
57 if (str.length * 3 <= maxLength) {
58 // too small to exceed maxLength
59 } else if ((lazyUtf8Len ??= utf8Len(str)) > maxLength) {
60 return ctx.issueTooBig(str, "string", maxLength, lazyUtf8Len);
61 }
62 }
63
64 let lazyGraphLen: number;
65
66 const { minGraphemes } = options;
67 if (minGraphemes != null) {
68 if (str.length < minGraphemes) {
69 return ctx.issueTooSmall(str, "grapheme", minGraphemes, str.length);
70 } else if ((lazyGraphLen ??= graphemeLen(str)) < minGraphemes) {
71 return ctx.issueTooSmall(str, "grapheme", minGraphemes, lazyGraphLen);
72 }
73 }
74
75 const { maxGraphemes } = options;
76 if (maxGraphemes != null) {
77 if ((lazyGraphLen ??= graphemeLen(str)) > maxGraphemes) {
78 return ctx.issueTooBig(str, "grapheme", maxGraphemes, lazyGraphLen);
79 }
80 }
81
82 if (options.format !== undefined) {
83 try {
84 assertStringFormat(str, options.format);
85 } catch (err) {
86 const message = err instanceof Error ? err.message : undefined;
87 return ctx.issueInvalidFormat(str, options.format, message);
88 }
89 }
90
91 return ctx.success(str as StringSchemaOutput<Options>);
92 }
93}
94
95export function coerceToString(input: unknown): string | null {
96 switch (typeof input) {
97 case "string":
98 return input;
99 case "object": {
100 if (input == null) return null;
101
102 if (input instanceof TokenSchema) {
103 return input.toString();
104 }
105
106 if (input instanceof Date) {
107 if (Number.isNaN(input.getTime())) return null;
108 return input.toISOString();
109 }
110
111 if (input instanceof URL) {
112 return input.toString();
113 }
114
115 const cid = asCid(input);
116 if (cid) return cid.toString();
117
118 if (input instanceof String) {
119 return input.valueOf();
120 }
121 }
122 // falls through
123 default:
124 return null;
125 }
126}