1
- const $RefParser = require ( "@apidevtools/json-schema-ref-parser" ) ;
2
1
const Ajv = require ( 'ajv' ) ;
2
+ const axios = require ( 'axios' ) ;
3
3
const formats = require ( 'ajv-formats-draft2019/formats' ) ;
4
4
const iriFormats = require ( './iri.js' ) ;
5
5
const fs = require ( 'fs-extra' ) ;
@@ -11,35 +11,15 @@ const {diffStringsUnified} = require('jest-diff');
11
11
const package = require ( './package.json' ) ;
12
12
13
13
let DEBUG = false ;
14
- let COMPILED = { } ;
15
- let SHORTCUTS = [
16
- 'checksum' , // legacy
17
- 'collection-assets' , // now in core
18
- 'datacube' , // now in stac-extensions org
19
- 'eo' ,
20
- 'item-assets' , // now in stac-extensions org
21
- 'label' , // now in stac-extensions org
22
- 'pointcloud' , // now in stac-extensions org
23
- 'processing' , // now in stac-extensions org
24
- 'projection' ,
25
- 'sar' , // now in stac-extensions org
26
- 'sat' , // now in stac-extensions org
27
- 'scientific' ,
28
- 'single-file-stac' , // now in stac-extensions org
29
- 'tiled-assets' , // now in stac-extensions org
30
- 'timestamps' , // now in stac-extensions org
31
- 'version' , // now in stac-extensions org
32
- 'view'
33
- ] ;
34
14
let ajv = new Ajv ( {
35
15
formats : Object . assign ( formats , iriFormats ) ,
36
16
allErrors : true ,
37
- missingRefs : "ignore" ,
38
- addUsedSchema : false ,
39
- logger : DEBUG ? console : false
17
+ logger : DEBUG ? console : false ,
18
+ loadSchema : loadJsonFromUri
40
19
} ) ;
41
20
let verbose = false ;
42
21
let schemaMap = { } ;
22
+ let schemaFolder = null ;
43
23
44
24
async function run ( ) {
45
25
console . log ( `STAC Node Validator v${ package . version } \n` ) ;
@@ -64,14 +44,13 @@ async function run() {
64
44
process . env [ "NODE_TLS_REJECT_UNAUTHORIZED" ] = 0 ;
65
45
}
66
46
67
- let schemaFolder = null ;
68
47
if ( typeof args . schemas === 'string' ) {
69
48
let stat = await fs . lstat ( args . schemas ) ;
70
49
if ( stat . isDirectory ( ) ) {
71
- schemaFolder = args . schemas ;
50
+ schemaFolder = normalizePath ( args . schemas ) ;
72
51
}
73
52
else {
74
- throw new Error ( 'Schema folder is not a valid directory' ) ;
53
+ throw new Error ( 'Schema folder is not a valid STAC directory' ) ;
75
54
}
76
55
}
77
56
@@ -109,38 +88,36 @@ async function run() {
109
88
let json ;
110
89
console . log ( `- ${ file } ` ) ;
111
90
try {
112
- if ( isUrl ( file ) ) {
113
- // For simplicity, we just load the URLs with $RefParser, so we don't need another dependency.
114
- json = await $RefParser . parse ( file ) ;
115
- if ( doLint ) {
116
- console . warn ( "-- Linting not supported for remote files" ) ;
117
- }
118
- if ( doFormat ) {
119
- console . warn ( "-- Formatting not supported for remote files" ) ;
120
- }
121
- }
122
- else {
91
+ let fileIsUrl = isUrl ( file ) ;
92
+ if ( ! fileIsUrl && ( doLint || doFormat ) ) {
123
93
let fileContent = await fs . readFile ( file , "utf8" ) ;
124
94
json = JSON . parse ( fileContent ) ;
125
- if ( doLint || doFormat ) {
126
- const expectedContent = JSON . stringify ( json , null , 2 ) ;
127
- if ( ! matchFile ( fileContent , expectedContent ) ) {
128
- stats . malformed ++ ;
129
- if ( doLint ) {
130
- console . warn ( "-- Lint: File is malformed -> use `--format` to fix the issue" ) ;
131
- if ( verbose ) {
132
- console . log ( diffStringsUnified ( fileContent , expectedContent ) ) ;
133
- }
134
- }
135
- if ( doFormat ) {
136
- console . warn ( "-- Format: File was malformed -> fixed the issue" ) ;
137
- await fs . writeFile ( file , expectedContent ) ;
95
+ const expectedContent = JSON . stringify ( json , null , 2 ) ;
96
+ if ( ! matchFile ( fileContent , expectedContent ) ) {
97
+ stats . malformed ++ ;
98
+ if ( doLint ) {
99
+ console . warn ( "-- Lint: File is malformed -> use `--format` to fix the issue" ) ;
100
+ if ( verbose ) {
101
+ console . log ( diffStringsUnified ( fileContent , expectedContent ) ) ;
138
102
}
139
103
}
140
- else if ( doLint && verbose ) {
141
- console . warn ( "-- Lint: File is well-formed" ) ;
104
+ if ( doFormat ) {
105
+ console . warn ( "-- Format: File was malformed -> fixed the issue" ) ;
106
+ await fs . writeFile ( file , expectedContent ) ;
142
107
}
143
108
}
109
+ else if ( doLint && verbose ) {
110
+ console . warn ( "-- Lint: File is well-formed" ) ;
111
+ }
112
+ }
113
+ else {
114
+ json = await loadJsonFromUri ( file ) ;
115
+ if ( fileIsUrl && ( doLint || doFormat ) ) {
116
+ let what = [ ] ;
117
+ doLint && what . push ( 'Linting' ) ;
118
+ doLint && what . push ( 'Formatting' ) ;
119
+ console . warn ( `-- ${ what . join ( ' and ' ) } not supported for remote files` ) ;
120
+ }
144
121
}
145
122
}
146
123
catch ( error ) {
@@ -187,53 +164,67 @@ async function run() {
187
164
fileValid = false ;
188
165
continue ;
189
166
}
190
- else if ( versions . compare ( data . stac_version , '1.0.0-beta.2 ' , '<' ) ) {
191
- console . error ( `-- ${ id } Skipping; Can only validate STAC version >= 1.0.0-beta.2 \n` ) ;
167
+ else if ( versions . compare ( data . stac_version , '1.0.0-rc.1 ' , '<' ) ) {
168
+ console . error ( `-- ${ id } Skipping; Can only validate STAC version >= 1.0.0-rc.1 \n` ) ;
192
169
continue ;
193
170
}
194
171
else if ( verbose ) {
195
172
console . log ( `-- ${ id } STAC Version: ${ data . stac_version } ` ) ;
196
173
}
197
174
198
- let type ;
199
- if ( data . type === 'Feature' ) {
200
- type = 'item' ;
201
- }
202
- else if ( data . type === 'FeatureCollection' ) {
203
- // type = 'itemcollection';
204
- console . warn ( `-- ${ id } Skipping; STAC ItemCollections not supported yet\n` ) ;
205
- continue ;
206
- }
207
- else if ( data . type === "Collection" || typeof data . extent !== 'undefined' || typeof data . license !== 'undefined' ) {
208
- type = 'collection' ;
209
-
210
- }
211
- else if ( data . type === "Catalog" || typeof data . description !== 'undefined' ) {
212
- type = 'catalog' ;
213
- }
214
- else {
215
- console . error ( `-- ${ id } Invalid; Can't detect which schema to use.\n` ) ;
216
- fileValid = false ;
217
- continue ;
175
+ switch ( data . type ) {
176
+ case 'FeatureCollection' :
177
+ console . warn ( `-- ${ id } Skipping; STAC ItemCollections not supported yet\n` ) ;
178
+ continue ;
179
+ case 'Catalog' :
180
+ case 'Collection' :
181
+ case 'Feature' :
182
+ break ;
183
+ default :
184
+ console . error ( `-- ${ id } Invalid; Can't detect type of the STAC object. Is the 'type' field missing or invalid?\n` ) ;
185
+ fileValid = false ;
186
+ continue ;
218
187
}
219
188
220
189
// Get all schema to validate against
221
- let schemas = [ type ] ;
190
+ let schemas = [ data . type ] ;
222
191
if ( Array . isArray ( data . stac_extensions ) ) {
223
192
schemas = schemas . concat ( data . stac_extensions ) ;
193
+ // Convert shortcuts supported in 1.0.0 RC1 into schema URLs
194
+ if ( versions . compare ( data . stac_version , '1.0.0-rc.1' , '=' ) ) {
195
+ schemas = schemas . map ( ext => ext . replace ( / ^ ( e o | p r o j e c t i o n | s c i e n t i f i c | v i e w ) $ / , 'https://schemas.stacspec.org/v1.0.0-rc.1/extensions/$1/json-schema/schema.json' ) ) ;
196
+ }
224
197
}
225
198
226
199
for ( let schema of schemas ) {
227
200
try {
228
- let loadArgs = isUrl ( schema ) ? [ schema ] : [ schemaFolder , data . stac_version , schema ] ;
229
- let validate = await loadSchema ( ...loadArgs ) ;
201
+ let schemaId ;
202
+ let core = false ;
203
+ switch ( schema ) {
204
+ case 'Feature' :
205
+ schema = 'Item' ;
206
+ case 'Catalog' :
207
+ case 'Collection' :
208
+ let type = schema . toLowerCase ( ) ;
209
+ schemaId = `https://schemas.stacspec.org/v${ data . stac_version } /${ type } -spec/json-schema/${ type } .json` ;
210
+ core = true ;
211
+ break ;
212
+ default : // extension
213
+ if ( isUrl ( schema ) ) {
214
+ schemaId = schema ;
215
+ }
216
+ else {
217
+ throw new Error ( "'stac_extensions' must contain a valid schema URL, not a shortcut." ) ;
218
+ }
219
+ }
220
+ let validate = await loadSchema ( schemaId ) ;
230
221
let valid = validate ( data ) ;
231
222
if ( ! valid ) {
232
223
console . log ( `--- ${ schema } : invalid` ) ;
233
224
console . warn ( validate . errors ) ;
234
225
console . log ( "\n" ) ;
235
226
fileValid = false ;
236
- if ( schema === ' core' && ! DEBUG ) {
227
+ if ( core && ! DEBUG ) {
237
228
if ( verbose ) {
238
229
console . info ( "-- Validation error in core, skipping extension validation" ) ;
239
230
}
@@ -278,18 +269,24 @@ function matchFile(given, expected) {
278
269
return normalizeNewline ( given ) === normalizeNewline ( expected ) ;
279
270
}
280
271
272
+ function normalizePath ( path ) {
273
+ return path . replace ( / \\ / g, '/' ) . replace ( / \/ $ / , "" ) ;
274
+ }
275
+
281
276
function normalizeNewline ( str ) {
282
277
// 2 spaces, *nix newlines, newline at end of file
283
278
return str . trimRight ( ) . replace ( / ( \r \n | \r ) / g, "\n" ) + "\n" ;
284
279
}
285
280
286
281
function isUrl ( uri ) {
287
- let part = uri . match ( / ^ ( \w + ) : \/ \/ / i) ;
288
- if ( part ) {
289
- if ( ! SUPPORTED_PROTOCOLS . includes ( part [ 1 ] . toLowerCase ( ) ) ) {
290
- throw new Error ( `Given protocol "${ part [ 1 ] } " is not supported.` ) ;
282
+ if ( typeof uri === 'string' ) {
283
+ let part = uri . match ( / ^ ( \w + ) : \/ \/ / i) ;
284
+ if ( part ) {
285
+ if ( ! SUPPORTED_PROTOCOLS . includes ( part [ 1 ] . toLowerCase ( ) ) ) {
286
+ throw new Error ( `Given protocol "${ part [ 1 ] } " is not supported.` ) ;
287
+ }
288
+ return true ;
291
289
}
292
- return true ;
293
290
}
294
291
return false ;
295
292
}
@@ -305,68 +302,43 @@ async function readExamples(folder) {
305
302
return files ;
306
303
}
307
304
308
- async function loadSchema ( baseUrl = null , version = null , shortcut = null ) {
309
- version = ( typeof version === 'string' ) ? "v" + version : "unversioned" ;
310
-
311
- if ( typeof baseUrl !== 'string' ) {
312
- baseUrl = `https://schemas.stacspec.org/${ version } ` ;
305
+ async function loadJsonFromUri ( uri ) {
306
+ if ( schemaMap [ uri ] ) {
307
+ uri = schemaMap [ uri ] ;
313
308
}
314
- else {
315
- baseUrl = baseUrl . replace ( / \\ / g, '/' ) . replace ( / \/ $ / , "" ) ;
316
- }
317
-
318
- let url ;
319
- let isExtension = false ;
320
- if ( shortcut === 'item' || shortcut === 'catalog' || shortcut === 'collection' ) {
321
- url = `${ baseUrl } /${ shortcut } -spec/json-schema/${ shortcut } .json` ;
309
+ else if ( schemaFolder ) {
310
+ uri = uri . replace ( / ^ h t t p s : \/ \/ s c h e m a s \. s t a c s p e c \. o r g \/ v [ ^ \/ ] + / , schemaFolder ) ;
322
311
}
323
- else if ( typeof shortcut === 'string' ) {
324
- if ( shortcut === 'proj' ) {
325
- // Capture a very common mistake and give a better explanation (see #4)
326
- throw new Error ( "'stac_extensions' must contain 'projection instead of 'proj'." ) ;
327
- }
328
- url = `${ baseUrl } /extensions/${ shortcut } /json-schema/schema.json` ;
329
- isExtension = true ;
312
+ if ( isUrl ( uri ) ) {
313
+ let response = await axios . get ( uri ) ;
314
+ return response . data ;
330
315
}
331
316
else {
332
- url = baseUrl ;
317
+ return JSON . parse ( await fs . readFile ( uri , "utf8" ) ) ;
333
318
}
319
+ }
334
320
335
- if ( schemaMap [ url ] ) {
336
- url = schemaMap [ url ] ;
321
+ async function loadSchema ( schemaId ) {
322
+ let schema = ajv . getSchema ( schemaId ) ;
323
+ if ( schema ) {
324
+ return schema ;
337
325
}
338
326
339
- if ( typeof COMPILED [ url ] !== 'undefined' ) {
340
- return COMPILED [ url ] ;
341
- }
342
- else {
343
- try {
344
- let parser = new $RefParser ( ) ;
345
- let fullSchema = await parser . dereference ( url , {
346
- dereference : {
347
- circular : 'ignore'
348
- }
349
- } ) ;
350
- COMPILED [ url ] = ajv . compile ( fullSchema ) ;
351
- if ( parser . $refs . circular && verbose ) {
352
- console . log ( `--- Schema ${ url } is circular, which is not supported by the library. Some properties may not get validated.` ) ;
353
- }
354
- return COMPILED [ url ] ;
355
- } catch ( error ) {
356
- // Convert error to string, both for Error objects and strings
357
- let msg = "" + error ;
358
- // Give better error message for (likely) invalid shortcuts
359
- if ( isExtension && ! SHORTCUTS . includes ( shortcut ) && ( msg . includes ( "Error downloading" ) || msg . includes ( "Error opening file" ) ) ) {
360
- if ( DEBUG ) {
361
- console . trace ( error ) ;
362
- }
363
- throw new Error ( `-- Schema at '${ url } ' not found. Please ensure all entries in 'stac_extensions' are valid.` ) ;
364
- }
365
- else {
366
- throw error ;
367
- }
327
+ try {
328
+ json = await loadJsonFromUri ( schemaId ) ;
329
+ } catch ( error ) {
330
+ if ( DEBUG ) {
331
+ console . trace ( error ) ;
368
332
}
333
+ throw new Error ( `-- Schema at '${ schemaId } ' not found. Please ensure all entries in 'stac_extensions' are valid.` ) ;
369
334
}
335
+
336
+ schema = ajv . getSchema ( json . $id ) ;
337
+ if ( schema ) {
338
+ return schema ;
339
+ }
340
+
341
+ return await ajv . compileAsync ( json ) ;
370
342
}
371
343
372
344
module . exports = async ( ) => {
0 commit comments