1
1
import { t } from "@/i18n" ;
2
2
import { GroupVariant , type Query , ShowMetadataVariant , SortingVariant } from "@/query/query" ;
3
3
import YAML from "yaml" ;
4
+ import { z } from "zod" ;
4
5
5
6
export class ParsingError extends Error {
7
+ messages : string [ ] ;
6
8
inner : unknown | undefined ;
7
9
8
- constructor ( msg : string , inner : unknown | undefined = undefined ) {
9
- super ( msg ) ;
10
+ constructor ( msgs : string [ ] , inner : unknown | undefined = undefined ) {
11
+ super ( msgs . join ( "\n" ) ) ;
10
12
this . inner = inner ;
13
+ this . messages = msgs ;
11
14
}
12
15
13
16
public toString ( ) : string {
@@ -22,7 +25,7 @@ export class ParsingError extends Error {
22
25
export type QueryWarning = string ;
23
26
24
27
export function parseQuery ( raw : string ) : [ Query , QueryWarning [ ] ] {
25
- let obj : Record < string , unknown > ;
28
+ let obj : Record < string , unknown > | null = null ;
26
29
const warnings : QueryWarning [ ] = [ ] ;
27
30
28
31
try {
@@ -32,11 +35,15 @@ export function parseQuery(raw: string): [Query, QueryWarning[]] {
32
35
try {
33
36
obj = tryParseAsYaml ( raw ) ;
34
37
} catch ( e ) {
35
- throw new ParsingError ( "Unable to parse as YAML or JSON" ) ;
38
+ throw new ParsingError ( [ "Unable to parse as YAML or JSON" ] ) ;
36
39
}
37
40
}
38
41
39
- const [ query , parsingWarnings ] = parseObject ( obj ) ;
42
+ if ( obj === null ) {
43
+ obj = { } ;
44
+ }
45
+
46
+ const [ query , parsingWarnings ] = parseObjectZod ( obj ) ;
40
47
warnings . push ( ...parsingWarnings ) ;
41
48
42
49
return [ query , warnings ] ;
@@ -46,169 +53,25 @@ function tryParseAsJson(raw: string): Record<string, unknown> {
46
53
try {
47
54
return JSON . parse ( raw ) ;
48
55
} catch ( e ) {
49
- throw new ParsingError ( "Invalid JSON" , e ) ;
56
+ throw new ParsingError ( [ "Invalid JSON" ] , e ) ;
50
57
}
51
58
}
52
59
53
60
function tryParseAsYaml ( raw : string ) : Record < string , unknown > {
54
61
try {
55
62
return YAML . parse ( raw ) ;
56
63
} catch ( e ) {
57
- throw new ParsingError ( "Invalid YAML" , e ) ;
58
- }
59
- }
60
-
61
- const validQueryKeys = new Set ( [ "name" , "filter" , "autorefresh" , "sorting" , "show" , "groupBy" ] ) ;
62
-
63
- function parseObject ( query : Record < string , unknown > ) : [ Query , QueryWarning [ ] ] {
64
- const warnings : QueryWarning [ ] = [ ] ;
65
-
66
- for ( const key of Object . keys ( query ) ) {
67
- if ( ! validQueryKeys . has ( key ) ) {
68
- warnings . push ( t ( ) . query . warning . unknownKey ( key ) ) ;
69
- }
70
- }
71
-
72
- return [
73
- {
74
- name : stringField ( query , "name" ) ?? "" ,
75
- filter : asRequired ( "filter" , stringField ( query , "filter" ) ) ,
76
- autorefresh : numberField ( query , "autorefresh" , { isPositive : true } ) ?? 0 ,
77
- sorting : optionsArrayField ( query , "sorting" , sortingLookup ) ?? [ SortingVariant . Order ] ,
78
- show : new Set (
79
- optionsArrayField ( query , "show" , showMetadataVariantLookup ) ??
80
- Object . values ( showMetadataVariantLookup ) ,
81
- ) ,
82
- groupBy : optionField ( query , "groupBy" , groupByVariantLookup ) ?? GroupVariant . None ,
83
- } ,
84
- warnings ,
85
- ] ;
86
- }
87
-
88
- function asRequired < T > ( key : string , val : T | undefined ) : T {
89
- if ( val === undefined ) {
90
- throw new ParsingError ( `Field ${ key } must be text` ) ;
91
- }
92
-
93
- return val as T ;
94
- }
95
-
96
- function stringField ( obj : Record < string , unknown > , key : string ) : string | undefined {
97
- const value = obj [ key ] ;
98
-
99
- if ( value === undefined ) {
100
- return undefined ;
101
- }
102
-
103
- if ( typeof value !== "string" ) {
104
- throw new ParsingError ( `Field ${ key } must be text` ) ;
105
- }
106
-
107
- return value as string ;
108
- }
109
-
110
- function numberField (
111
- obj : Record < string , unknown > ,
112
- key : string ,
113
- options ?: { isPositive : boolean } ,
114
- ) : number | undefined {
115
- const value = obj [ key ] ;
116
-
117
- if ( value === undefined ) {
118
- return undefined ;
119
- }
120
-
121
- if ( typeof value !== "number" ) {
122
- throw new ParsingError ( `Field ${ key } must be a number` ) ;
123
- }
124
-
125
- const num = value as number ;
126
-
127
- if ( Number . isNaN ( num ) ) {
128
- throw new ParsingError ( `Field ${ key } must be a number` ) ;
129
- }
130
-
131
- if ( ( options ?. isPositive ?? false ) && num < 0 ) {
132
- throw new ParsingError ( `Field ${ key } must be a positive number` ) ;
133
- }
134
-
135
- return num ;
136
- }
137
-
138
- function booleanField ( obj : Record < string , unknown > , key : string ) : boolean | undefined {
139
- const value = obj [ key ] ;
140
-
141
- if ( value === undefined ) {
142
- return undefined ;
143
- }
144
-
145
- if ( typeof value !== "boolean" ) {
146
- throw new ParsingError ( `Field ${ key } must be a boolean.` ) ;
64
+ throw new ParsingError ( [ "Invalid YAML" ] , e ) ;
147
65
}
148
-
149
- return value as boolean ;
150
66
}
151
67
152
- function optionsArrayField < T > (
153
- obj : Record < string , unknown > ,
154
- key : string ,
155
- lookup : Record < string , T > ,
156
- ) : T [ ] | undefined {
157
- const value = obj [ key ] ;
158
-
159
- if ( value === undefined ) {
160
- return undefined ;
161
- }
162
-
163
- const opts = Object . keys ( lookup ) . join ( ", " ) ;
164
- if ( ! Array . isArray ( value ) ) {
165
- throw new ParsingError ( `Field ${ key } must be an array from values: ${ opts } ` ) ;
166
- }
167
-
168
- const elems = value as Record < string , unknown > [ ] ;
169
- const parsedElems = [ ] ;
170
-
171
- for ( const ele of elems ) {
172
- if ( typeof ele !== "string" ) {
173
- throw new ParsingError ( `Field ${ key } must be an array from values: ${ opts } ` ) ;
174
- }
175
-
176
- const lookupValue = lookup [ ele ] ;
177
- if ( lookupValue === undefined ) {
178
- throw new ParsingError ( `Field ${ key } must be an array from values: ${ opts } ` ) ;
179
- }
180
-
181
- parsedElems . push ( lookupValue ) ;
182
- }
183
-
184
- return parsedElems ;
185
- }
186
-
187
- function optionField < T > (
188
- obj : Record < string , unknown > ,
189
- key : string ,
190
- lookup : Record < string , T > ,
191
- ) : T | undefined {
192
- const value = obj [ key ] ;
193
-
194
- if ( value === undefined ) {
195
- return undefined ;
196
- }
197
-
198
- const opts = Object . keys ( lookup ) . join ( ", " ) ;
199
- if ( typeof value !== "string" ) {
200
- throw new ParsingError ( `Field ${ key } must be one of: ${ opts } ` ) ;
201
- }
202
-
203
- const lookupValue = lookup [ value ] ;
204
- if ( lookupValue === undefined ) {
205
- throw new ParsingError ( `Field ${ key } must be one of: ${ opts } ` ) ;
206
- }
207
-
208
- return lookupValue ;
209
- }
68
+ const lookupToEnum = < T > ( lookup : Record < string , T > ) => {
69
+ const keys = Object . keys ( lookup ) ;
70
+ //@ts -ignore: There is at least one element for these.
71
+ return z . enum ( keys ) . transform ( ( key ) => lookup [ key ] ) ;
72
+ } ;
210
73
211
- const sortingLookup : Record < string , SortingVariant > = {
74
+ const sortingSchema = lookupToEnum ( {
212
75
priority : SortingVariant . Priority ,
213
76
priorityAscending : SortingVariant . PriorityAscending ,
214
77
priorityDescending : SortingVariant . Priority ,
@@ -219,21 +82,93 @@ const sortingLookup: Record<string, SortingVariant> = {
219
82
dateAdded : SortingVariant . DateAdded ,
220
83
dateAddedAscending : SortingVariant . DateAdded ,
221
84
dateAddedDescending : SortingVariant . DateAddedDescending ,
222
- } ;
85
+ } ) ;
223
86
224
- const showMetadataVariantLookup : Record < string , ShowMetadataVariant > = {
87
+ const showSchema = lookupToEnum ( {
225
88
due : ShowMetadataVariant . Due ,
226
89
date : ShowMetadataVariant . Due ,
227
90
description : ShowMetadataVariant . Description ,
228
91
labels : ShowMetadataVariant . Labels ,
229
92
project : ShowMetadataVariant . Project ,
230
- } ;
93
+ } ) ;
231
94
232
- const groupByVariantLookup : Record < string , GroupVariant > = {
95
+ const groupBySchema = lookupToEnum ( {
233
96
project : GroupVariant . Project ,
234
97
section : GroupVariant . Section ,
235
98
priority : GroupVariant . Priority ,
236
99
due : GroupVariant . Date ,
237
100
date : GroupVariant . Date ,
238
101
labels : GroupVariant . Label ,
102
+ } ) ;
103
+
104
+ const defaults = {
105
+ name : "" ,
106
+ autorefresh : 0 ,
107
+ sorting : [ SortingVariant . Order ] ,
108
+ show : [
109
+ ShowMetadataVariant . Due ,
110
+ ShowMetadataVariant . Description ,
111
+ ShowMetadataVariant . Labels ,
112
+ ShowMetadataVariant . Project ,
113
+ ] ,
114
+ groupBy : GroupVariant . None ,
239
115
} ;
116
+
117
+ const querySchema = z . object ( {
118
+ name : z . string ( ) . optional ( ) . default ( "" ) ,
119
+ filter : z . string ( ) ,
120
+ autorefresh : z . number ( ) . nonnegative ( ) . optional ( ) . default ( 0 ) ,
121
+ sorting : z
122
+ . array ( sortingSchema )
123
+ . optional ( )
124
+ . transform ( ( val ) => val ?? defaults . sorting ) ,
125
+ show : z
126
+ . array ( showSchema )
127
+ . optional ( )
128
+ . transform ( ( val ) => val ?? defaults . show ) ,
129
+ groupBy : groupBySchema . optional ( ) . transform ( ( val ) => val ?? defaults . groupBy ) ,
130
+ } ) ;
131
+
132
+ const validQueryKeys : string [ ] = querySchema . keyof ( ) . options ;
133
+
134
+ function parseObjectZod ( query : Record < string , unknown > ) : [ Query , QueryWarning [ ] ] {
135
+ const warnings : QueryWarning [ ] = [ ] ;
136
+
137
+ for ( const key of Object . keys ( query ) ) {
138
+ if ( ! validQueryKeys . includes ( key ) ) {
139
+ warnings . push ( t ( ) . query . warning . unknownKey ( key ) ) ;
140
+ }
141
+ }
142
+
143
+ const out = querySchema . safeParse ( query ) ;
144
+
145
+ if ( ! out . success ) {
146
+ throw new ParsingError ( formatZodError ( out . error ) ) ;
147
+ }
148
+
149
+ return [
150
+ {
151
+ name : out . data . name ,
152
+ filter : out . data . filter ,
153
+ autorefresh : out . data . autorefresh ,
154
+ sorting : out . data . sorting ,
155
+ show : new Set ( out . data . show ) ,
156
+ groupBy : out . data . groupBy ,
157
+ } ,
158
+ warnings ,
159
+ ] ;
160
+ }
161
+
162
+ function formatZodError ( error : z . ZodError ) : string [ ] {
163
+ return error . errors . map ( ( err ) => {
164
+ const field = err . path [ 0 ] ;
165
+ switch ( err . code ) {
166
+ case "invalid_type" :
167
+ return `Field '${ field } ' is ${ err . received === "undefined" ? "required" : `must be a ${ err . expected } ` } ` ;
168
+ case "invalid_enum_value" :
169
+ return `Field '${ field } ' has invalid value '${ err . received } '. Valid options are: ${ err . options ?. join ( ", " ) } ` ;
170
+ default :
171
+ return `Field '${ field } ': ${ err . message } ` ;
172
+ }
173
+ } ) ;
174
+ }
0 commit comments