-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- [Original package for Node.js](https://github.com/jsontypedef/json-typedef-js) - Flatten directories to match typical Deno packages - Rewrite tests using the Deno testing suite - Inline test cases
- Loading branch information
0 parents
commit b2050a4
Showing
10 changed files
with
7,210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Original work copyright (c) 2020 JSON Type Definition Contributors | ||
Modified work copyright (c) 2021 Benjamin Herman | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
# jtd: JSON Validation for JavaScript | ||
|
||
Deno port of the [Node.js package of the same name][repo]. | ||
|
||
[JSON Type Definition][jtd], aka [RFC 8927], is an easy-to-learn, standardized | ||
way to define a schema for JSON data. You can use JSON Typedef to portably | ||
validate data across programming languages, create dummy data, generate code, | ||
and more. | ||
|
||
This `jtd` package is a TypeScript implementation of JSON Type Definition. It | ||
lets you validate input data against JSON Type Definition schemas. This ported | ||
version of `jtd` works in Deno, but the [original][repo] works in Node.js and | ||
web browsers. | ||
|
||
If you're looking to generate code from schemas, check out "Generating | ||
TypeScript from JSON Typedef schemas" in the JSON Typedef docs. | ||
|
||
## Importing | ||
|
||
```js | ||
import * from "https://deno.land/x/[email protected]/mod.ts"; | ||
``` | ||
|
||
## Documentation | ||
|
||
Detailed API documentation is available online at: | ||
|
||
https://doc.deno.land/https/deno.land/x/[email protected]/mod.ts | ||
|
||
For more high-level documentation about JSON Typedef in general, or JSON Typedef | ||
in combination with JavaScript in particular, see: | ||
|
||
- [The JSON Typedef Website][jtd] | ||
- ["Generating TypeScript from JSON Typedef Schemas"][jtd-ts-codegen] | ||
|
||
## Basic Usage | ||
|
||
Here's an example of how you can use this package to validate JSON data against | ||
a JSON Typedef schema: | ||
|
||
```ts | ||
import { Schema, validate } from "https://deno.land/x/[email protected]/mod.ts"; | ||
|
||
const schema = { | ||
properties: { | ||
name: { type: "string" }, | ||
age: { type: "uint32" }, | ||
phones: { | ||
elements: { type: "string" }, | ||
}, | ||
}, | ||
} as Schema; | ||
|
||
// jtd.validate returns an array of validation errors. If there were no problems | ||
// with the input, it returns an empty array. | ||
|
||
// Outputs: [] | ||
console.log(validate(schema, { | ||
name: "John Doe", | ||
age: 43, | ||
phones: ["+44 1234567", "+44 2345678"], | ||
})); | ||
|
||
// This next input has three problems with it: | ||
// | ||
// 1. It's missing "name", which is a required property. | ||
// 2. "age" is a string, but it should be an integer. | ||
// 3. "phones[1]" is a number, but it should be a string. | ||
// | ||
// Each of those errors corresponds to one of the errors returned by validate. | ||
|
||
// Outputs: | ||
// | ||
// [ | ||
// { instancePath: [], schemaPath: [ 'properties', 'name' ] }, | ||
// { | ||
// instancePath: [ 'age' ], | ||
// schemaPath: [ 'properties', 'age', 'type' ] | ||
// }, | ||
// { | ||
// instancePath: [ 'phones', '1' ], | ||
// schemaPath: [ 'properties', 'phones', 'elements', 'type' ] | ||
// } | ||
// ] | ||
console.log(validate(schema, { | ||
age: "43", | ||
phones: ["+44 1234567", 442345678], | ||
})); | ||
``` | ||
|
||
## Advanced Usage: Limiting Errors Returned | ||
|
||
By default, `jtd.validate` returns every error it finds. If you just care about | ||
whether there are any errors at all, or if you can't show more than some number | ||
of errors, then you can get better performance out of `jtd.validate` using the | ||
`maxErrors` option. | ||
|
||
For example, taking the same example from before, but limiting it to 1 error, we | ||
get: | ||
|
||
```ts | ||
// Outputs: | ||
// | ||
// [ { instancePath: [], schemaPath: [ 'properties', 'name' ] } ] | ||
console.log(validate(schema, { | ||
age: "43", | ||
phones: ["+44 1234567", 442345678], | ||
}, { maxErrors: 1 })); | ||
``` | ||
|
||
## Advanced Usage: Handling Untrusted Schemas | ||
|
||
If you want to run `jtd` against a schema that you don't trust, then you should: | ||
|
||
1. Ensure the schema is well-formed, using `jtd.isSchema` and | ||
`jtd.isValidSchema`. `isSchema` does basic "type" checking (and in | ||
TypeScript, it acts as a type guard for the `Schema` type), while | ||
`isValidSchema` validates things like making sure all `ref`s have | ||
corresponding definitions. | ||
|
||
2. Call `jtd.validate` with the `maxDepth` option. JSON Typedef lets you write | ||
recursive schemas -- if you're evaluating against untrusted schemas, you | ||
might go into an infinite loop when evaluating against a malicious input, | ||
such as this one: | ||
|
||
```json | ||
{ | ||
"ref": "loop", | ||
"definitions": { | ||
"loop": { | ||
"ref": "loop" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The `maxDepth` option tells `jtd.validate` how many `ref`s to follow | ||
recursively before giving up and throwing `jtd.MaxDepthExceededError`. | ||
|
||
Here's an example of how you can use `jtd` to evaluate data against an untrusted | ||
schema: | ||
|
||
```ts | ||
import { isSchema, isValidSchema, Schema, validate } from "jtd"; | ||
|
||
// validateUntrusted returns true if `data` satisfies `schema`, and false if it | ||
// does not. Throws an error if `schema` is invalid, or if validation goes in an | ||
// infinite loop. | ||
function validateUntrusted(schema: unknown, data: unknown): boolean { | ||
if (!isSchema(schema) || !isValidSchema(schema)) { | ||
throw new Error("invalid schema"); | ||
} | ||
|
||
// You should tune maxDepth to be high enough that most legitimate schemas | ||
// evaluate without errors, but low enough that an attacker cannot cause a | ||
// denial of service attack. | ||
return validate(schema, data, { maxDepth: 32 }).length === 0; | ||
} | ||
|
||
// Returns true | ||
validateUntrusted({ type: "string" }, "foo"); | ||
|
||
// Returns false | ||
validateUntrusted({ type: "string" }, null); | ||
|
||
// Throws "invalid schema" | ||
validateUntrusted({ type: "nonsense" }, null); | ||
|
||
// Throws an instance of jtd.MaxDepthExceededError | ||
validateUntrusted({ | ||
"ref": "loop", | ||
"definitions": { | ||
"loop": { | ||
"ref": "loop", | ||
}, | ||
}, | ||
}, null); | ||
``` | ||
|
||
## LICENSE | ||
|
||
[MIT](LICENSE.txt) | ||
|
||
[repo]: https://github.com/jsontypedef/json-typedef-js | ||
[RFC 8927]: https://tools.ietf.org/html/rfc8927 | ||
[jtd]: https://jsontypedef.com | ||
[jtd-ts-codegen]: https://jsontypedef.com/docs/typescript-codegen/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { isRFC3339 } from "./_rfc3339.ts"; | ||
import { assertEquals } from "./dev_deps.ts"; | ||
|
||
const testCases: [string, boolean][] = [ | ||
// From the RFC | ||
["1985-04-12T23:20:50.52Z", true], | ||
["1990-12-31T23:59:60Z", true], | ||
["1990-12-31T15:59:60-08:00", true], | ||
["1937-01-01T12:00:27.87+00:20", true], | ||
|
||
// T and Z can be t or z | ||
["1985-04-12t23:20:50.52z", true], | ||
|
||
// https://github.com/chronotope/chrono/blob/main/src/format/parse.rs | ||
["2015-01-20T17:35:20-08:00", true], // normal case | ||
["1944-06-06T04:04:00Z", true], // D-day | ||
["2001-09-11T09:45:00-08:00", true], | ||
["2015-01-20T17:35:20.001-08:00", true], | ||
["2015-01-20T17:35:20.000031-08:00", true], | ||
["2015-01-20T17:35:20.000000004-08:00", true], | ||
["2015-01-20T17:35:20.000000000452-08:00", true], // too small | ||
["2015-02-30T17:35:20-08:00", false], // bad day of month | ||
["2015-01-20T25:35:20-08:00", false], // bad hour | ||
["2015-01-20T17:65:20-08:00", false], // bad minute | ||
["2015-01-20T17:35:90-08:00", false], // bad second | ||
|
||
// Ensure the regex is anchored | ||
["x1985-04-12T23:20:50.52Zx", false], | ||
["1985-04-12T23:20:50.52Zx", false], | ||
]; | ||
|
||
Deno.test({ | ||
name: "isRFC3339", | ||
fn() { | ||
for (const [input, result] of testCases) { | ||
assertEquals(isRFC3339(input), result); | ||
} | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const pattern = | ||
/^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$/; | ||
|
||
export function isRFC3339(s: string): boolean { | ||
const matches = s.match(pattern); | ||
if (matches === null) { | ||
return false; | ||
} | ||
|
||
const year = parseInt(matches[1], 10); | ||
const month = parseInt(matches[2], 10); | ||
const day = parseInt(matches[3], 10); | ||
const hour = parseInt(matches[4], 10); | ||
const minute = parseInt(matches[5], 10); | ||
const second = parseInt(matches[6], 10); | ||
|
||
if (month > 12) { | ||
return false; | ||
} | ||
|
||
if (day > maxDay(year, month)) { | ||
return false; | ||
} | ||
|
||
if (hour > 23) { | ||
return false; | ||
} | ||
|
||
if (minute > 59) { | ||
return false; | ||
} | ||
|
||
// A value of 60 is permissible as a leap second. | ||
if (second > 60) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
function maxDay(year: number, month: number) { | ||
if (month === 2) { | ||
return isLeapYear(year) ? 29 : 28; | ||
} | ||
|
||
return MONTH_LENGTHS[month]; | ||
} | ||
|
||
function isLeapYear(n: number): boolean { | ||
return n % 4 === 0 && (n % 100 !== 0 || n % 400 === 0); | ||
} | ||
|
||
const MONTH_LENGTHS = [ | ||
0, // months are 1-indexed, this is a dummy element | ||
31, | ||
0, // Feb is handled separately | ||
31, | ||
30, | ||
31, | ||
30, | ||
31, | ||
31, | ||
30, | ||
31, | ||
30, | ||
31, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "https://deno.land/[email protected]/testing/asserts.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { | ||
isSchema, | ||
isValidSchema, | ||
MaxDepthExceededError, | ||
validate, | ||
} from "./mod.ts"; | ||
import { assert, assertEquals, assertThrows } from "./dev_deps.ts"; | ||
import { testCases } from "./test_cases.test.ts"; | ||
|
||
Deno.test({ | ||
name: "Schema validity", | ||
fn() { | ||
for (const { description, schema, validSchema } of testCases) { | ||
assertEquals( | ||
isSchema(schema) && isValidSchema(schema), | ||
validSchema, | ||
`${ | ||
validSchema ? "Valid" : "Invalid" | ||
} schema in test case with description "${description}" ${ | ||
validSchema ? "did not validate" : "was incorrectly validated" | ||
}.`, | ||
); | ||
} | ||
}, | ||
}); | ||
|
||
Deno.test({ | ||
name: "Validation supports limited depth", | ||
fn() { | ||
const schema = { | ||
definitions: { | ||
foo: { | ||
ref: "foo", | ||
}, | ||
}, | ||
ref: "foo", | ||
}; | ||
const instance = null; | ||
assertThrows( | ||
() => validate(schema, instance, { maxDepth: 5, maxErrors: 0 }), | ||
MaxDepthExceededError, | ||
); | ||
}, | ||
}); | ||
|
||
Deno.test({ | ||
name: "Validation supports limited errors", | ||
fn() { | ||
const schema = { | ||
elements: { | ||
type: "string" as const, | ||
}, | ||
}; | ||
const instance = [null, null, null, null, null]; | ||
assertEquals( | ||
validate(schema, instance, { maxDepth: 0, maxErrors: 3 }).length, | ||
3, | ||
); | ||
}, | ||
}); | ||
|
||
Deno.test({ | ||
name: "Validate test cases", | ||
fn() { | ||
const validTestCases = testCases.filter((testCase) => testCase.validSchema); | ||
for (const { description, schema, instance, errors } of validTestCases) { | ||
assert(isSchema(schema)); | ||
assertEquals( | ||
validate(schema, instance), | ||
errors, | ||
`${ | ||
errors.length ? "Invalid" : "Valid" | ||
} instance in test case with description "${description}" ${ | ||
errors.length ? "incorrectly validated" : "did not validate" | ||
}.`, | ||
); | ||
} | ||
}, | ||
}); |
Oops, something went wrong.