Blame Raw
· · · 169 lines (4.5 KB)
0 contributors
1/**
2 * Validates the data structure of character cards.
3 * Supported specs: V1, V2
4 * Up to: 8083fb3
5 *
6 * @link https://github.com/malfoyslastname/character-card-spec-v2
7 */
8export class TavernCardValidator {
9 /**
10 * @type {string|null}
11 */
12 #lastValidationError = null;
13
14 constructor(card) {
15 this.card = card;
16 }
17
18 /**
19 * Field that caused the validation to fail
20 *
21 * @returns {null|string}
22 */
23 get lastValidationError() {
24 return this.#lastValidationError;
25 }
26
27 /**
28 * Validate against V1 or V2 spec.
29 *
30 * @returns {number|boolean} - false when neither V1 nor V2 spec were matched. Specification version number otherwise.
31 */
32 validate() {
33 this.#lastValidationError = null;
34
35 if (this.validateV1()) {
36 return 1;
37 }
38
39 if (this.validateV2()) {
40 return 2;
41 }
42
43 if (this.validateV3()) {
44 return 3;
45 }
46
47 return false;
48 }
49
50 /**
51 * Validate against V1 specification
52 *
53 * @returns {this is string[]}
54 */
55 validateV1() {
56 const requiredFields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example'];
57 return requiredFields.every(field => {
58 if (!Object.hasOwn(this.card, field)) {
59 this.#lastValidationError = field;
60 return false;
61 }
62 return true;
63 });
64 }
65
66 /**
67 * Validate against V2 specification
68 *
69 * @returns {false|boolean|*}
70 */
71 validateV2() {
72 return this.#validateSpecV2()
73 && this.#validateSpecVersionV2()
74 && this.#validateDataV2()
75 && this.#validateCharacterBookV2();
76 }
77
78 /**
79 * Validate against V3 specification
80 * @returns {boolean}
81 */
82 validateV3() {
83 return this.#validateSpecV3()
84 && this.#validateSpecVersionV3()
85 && this.#validateDataV3();
86 }
87
88 #validateSpecV2() {
89 if (this.card.spec !== 'chara_card_v2') {
90 this.#lastValidationError = 'spec';
91 return false;
92 }
93 return true;
94 }
95
96 #validateSpecVersionV2() {
97 if (this.card.spec_version !== '2.0') {
98 this.#lastValidationError = 'spec_version';
99 return false;
100 }
101 return true;
102 }
103
104 #validateDataV2() {
105 const data = this.card.data;
106
107 if (!data) {
108 this.#lastValidationError = 'No tavern card data found';
109 return false;
110 }
111
112 const requiredFields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example', 'creator_notes', 'system_prompt', 'post_history_instructions', 'alternate_greetings', 'tags', 'creator', 'character_version', 'extensions'];
113 const isAllRequiredFieldsPresent = requiredFields.every(field => {
114 if (!Object.hasOwn(data, field)) {
115 this.#lastValidationError = `data.${field}`;
116 return false;
117 }
118 return true;
119 });
120
121 return isAllRequiredFieldsPresent && Array.isArray(data.alternate_greetings) && Array.isArray(data.tags) && typeof data.extensions === 'object';
122 }
123
124 #validateCharacterBookV2() {
125 const characterBook = this.card.data.character_book;
126
127 if (!characterBook) {
128 return true;
129 }
130
131 const requiredFields = ['extensions', 'entries'];
132 const isAllRequiredFieldsPresent = requiredFields.every(field => {
133 if (!Object.hasOwn(characterBook, field)) {
134 this.#lastValidationError = `data.character_book.${field}`;
135 return false;
136 }
137 return true;
138 });
139
140 return isAllRequiredFieldsPresent && Array.isArray(characterBook.entries) && typeof characterBook.extensions === 'object';
141 }
142
143 #validateSpecV3() {
144 if (this.card.spec !== 'chara_card_v3') {
145 this.#lastValidationError = 'spec';
146 return false;
147 }
148 return true;
149 }
150
151 #validateSpecVersionV3() {
152 if (Number(this.card.spec_version) < 3.0 || Number(this.card.spec_version) >= 4.0) {
153 this.#lastValidationError = 'spec_version';
154 return false;
155 }
156 return true;
157 }
158
159 #validateDataV3() {
160 const data = this.card.data;
161
162 if (!data || typeof data !== 'object') {
163 this.#lastValidationError = 'No tavern card data found';
164 return false;
165 }
166
167 return true;
168 }
169}