mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 07:02:03 -08:00
remove JSON validator
This commit is contained in:
parent
cecaf2c9db
commit
18f1a3350f
12 changed files with 0 additions and 2003 deletions
1
Makefile
1
Makefile
|
|
@ -124,7 +124,6 @@ build-launcher: check-arch
|
||||||
package-linux:
|
package-linux:
|
||||||
python3 scripts/package.py linux \
|
python3 scripts/package.py linux \
|
||||||
--includes \
|
--includes \
|
||||||
settings/omegacfg.jvv \
|
|
||||||
settings/properties.json \
|
settings/properties.json \
|
||||||
bundle/fontconfigs \
|
bundle/fontconfigs \
|
||||||
--version $(version) \
|
--version $(version) \
|
||||||
|
|
|
||||||
728
jsonvv/README.md
728
jsonvv/README.md
|
|
@ -1,728 +0,0 @@
|
||||||
# JSONvv
|
|
||||||
|
|
||||||
JSON value validator
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This is a simple JSON schema validator library. It was created for Camoufox to validate passed user configurations. Because I found it useful for other projects, I decided to extract it into a separate library.
|
|
||||||
|
|
||||||
JSONvv's syntax parser is written in pure Python. It does not rely on any dependencies.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th width="50%">Configuration</th>
|
|
||||||
<th width="50%">Validator</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width="50%">
|
|
||||||
|
|
||||||
```python
|
|
||||||
config = {
|
|
||||||
"username": "johndoe",
|
|
||||||
"email": "johndoe@example.com",
|
|
||||||
"age": 30,
|
|
||||||
"chat": "Hello world!",
|
|
||||||
"preferences": {
|
|
||||||
"notifications": True,
|
|
||||||
"theme": "dark"
|
|
||||||
},
|
|
||||||
"allowed_commands": [
|
|
||||||
"/help", "/time", "/weather"
|
|
||||||
],
|
|
||||||
"location": [40.7128, -74.0060],
|
|
||||||
"hobbies": [
|
|
||||||
{
|
|
||||||
"name": "Traveling",
|
|
||||||
"cities": ["Paris", "London"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "reading",
|
|
||||||
"hours": {
|
|
||||||
"Sunday": 2,
|
|
||||||
"Monday": 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</td>
|
|
||||||
<td width="50%">
|
|
||||||
|
|
||||||
```python
|
|
||||||
validator = {
|
|
||||||
"username": "str", # Basic username
|
|
||||||
"email": "str[/\S+@\S+\.\S+/]", # Validate emails
|
|
||||||
"age": "int[>=18]", # Age must be 18 or older
|
|
||||||
"chat": "str | nil", # Optional chat message
|
|
||||||
"preferences": {
|
|
||||||
"notifications": "bool",
|
|
||||||
"theme": "str[light, dark] | nil", # Optional theme
|
|
||||||
},
|
|
||||||
# Commands must start with "/", but not contain "sudo"
|
|
||||||
"allowed_commands": "array[str[/^//] - str[/sudo/]]",
|
|
||||||
# Validate coordinate ranges
|
|
||||||
"location": "tuple[double[-90 - 90], double[-180 - 180]]",
|
|
||||||
# Handle an array of hobby types
|
|
||||||
"hobbies": "array[@traveling | @other, >=1]",
|
|
||||||
"@traveling": {
|
|
||||||
# Require 1 or more cities/countries iff name is "Traveling"
|
|
||||||
"*name,type": "str[Traveling]",
|
|
||||||
"*cities,countries": "array[str[A-Za-z*], >=1]",
|
|
||||||
},
|
|
||||||
"@other": {
|
|
||||||
"name,type": "str - str[Traveling]", # Non-traveling types
|
|
||||||
# If hour(s) is specified, require days have >0 hours
|
|
||||||
"/hours?/": {
|
|
||||||
"*/day$/": "int[>0]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<hr width=50>
|
|
||||||
|
|
||||||
Then, validate the configuration like this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from jsonvv import JsonValidator, JvvRuntimeException
|
|
||||||
|
|
||||||
val = JsonValidator(validator)
|
|
||||||
try:
|
|
||||||
val.validate(config)
|
|
||||||
except JvvRuntimeException as exc:
|
|
||||||
print("Failed:", exc)
|
|
||||||
else:
|
|
||||||
print('Config is valid!')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Key Syntax](#key-syntax)
|
|
||||||
- [Regex patterns](#regex-patterns)
|
|
||||||
- [Lists of possible values](#lists-of-possible-values)
|
|
||||||
- [Required fields (`*`)](#required-fields-)
|
|
||||||
- [Grouping keys (`$`)](#grouping-keys-)
|
|
||||||
- [Supported Types](#supported-types)
|
|
||||||
- [String (`str`)](#string-str)
|
|
||||||
- [Integer (`int`)](#integer-int)
|
|
||||||
- [Double (`double`)](#double-double)
|
|
||||||
- [Boolean (`bool`)](#boolean-bool)
|
|
||||||
- [Array (`array`)](#array-array)
|
|
||||||
- [Tuple (`tuple`)](#tuple-tuple)
|
|
||||||
- [Nested Dictionaries](#nested-dictionaries)
|
|
||||||
- [Nil (`nil`)](#nil-nil)
|
|
||||||
- [Any (`any`)](#any-any)
|
|
||||||
- [Required fields (`*`)](#required-fields-)
|
|
||||||
- [Type References (`@`)](#type-references-)
|
|
||||||
- [Advanced Features](#advanced-features)
|
|
||||||
- [Subtracting Domains (`-`)](#subtracting-domains--)
|
|
||||||
- [Union Types (`|`)](#union-types-)
|
|
||||||
- [Conditional Ranges and Values](#conditional-ranges-and-values)
|
|
||||||
- [Error Handling](#error-handling)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keys Syntax
|
|
||||||
|
|
||||||
Dictionary keys can be specified in several possible ways:
|
|
||||||
|
|
||||||
- `"key": "type"`
|
|
||||||
- `"key1,key2,key3": "type"`
|
|
||||||
- `"/key\d+/": "type"`
|
|
||||||
- `"*required_key": "type"`
|
|
||||||
|
|
||||||
### Regex patterns
|
|
||||||
|
|
||||||
To use regex in a key, wrap it in `/ ... /`.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"/key\d+/": "type"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lists of possible values
|
|
||||||
|
|
||||||
To specify a list of keys, use a comma-separated string.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"key1,key2,key3": "type"
|
|
||||||
"/k[ey]{2}1/,key2": "type"
|
|
||||||
```
|
|
||||||
|
|
||||||
To escape a comma, use `!`.
|
|
||||||
|
|
||||||
### Required fields (`*`)
|
|
||||||
|
|
||||||
Fields marked with `*` are required. The validation will fail without them.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"*key1": "type"
|
|
||||||
"*/key\d+/": "type"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Grouping keys (`$`)
|
|
||||||
|
|
||||||
Fields that end with `$group_name` are grouped together. If one of the keys is set, all of the keys in the group must also be set as well.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"isEnabled$group1": "bool"
|
|
||||||
"value$group1": "int[>0]"
|
|
||||||
```
|
|
||||||
|
|
||||||
This will require both `value` is set if and only if `isEnabled` is set.
|
|
||||||
|
|
||||||
Multiple `$` can be used to create more complex group dependencies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Types
|
|
||||||
|
|
||||||
### String (`str`)
|
|
||||||
|
|
||||||
Represents a string value. Optionally, you can specify a regex pattern that the string must match.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Basic string: `"str"`
|
|
||||||
- With regex pattern: `"str[regex_pattern]"`
|
|
||||||
- The escape character for regex is `\`, and for commas is `_`.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
- `regex_pattern`: A regular expression that the string must match. If not specified, any string is accepted.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Basic string:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"username": "str"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts any string value for the key `username`.
|
|
||||||
|
|
||||||
2. String with regex pattern:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"fullname": "str[/[A-Z][a-z]+ [A-Z][a-z]+/]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a string that matches the pattern of a first and last name starting with uppercase letters.
|
|
||||||
|
|
||||||
### Integer (`int`)
|
|
||||||
|
|
||||||
Represents an integer value. You can specify conditions like exact values, ranges, and inequalities.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Basic integer: `"int"`
|
|
||||||
- With conditions: `"int[conditions]"`
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
- `conditions`: A comma-separated list of conditions.
|
|
||||||
|
|
||||||
**Condition Operators:**
|
|
||||||
|
|
||||||
- `==`: Equal to a specific value.
|
|
||||||
- `>=`: Greater than or equal to a value.
|
|
||||||
- `<=`: Less than or equal to a value.
|
|
||||||
- `>`: Greater than a value.
|
|
||||||
- `<`: Less than a value.
|
|
||||||
- `range`: A range between two values (inclusive).
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Basic integer:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"age": "int"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts any integer value for the key `age`.
|
|
||||||
|
|
||||||
2. Integer with conditions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"userage": "int[>=0, <=120]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integer values between 0 and 120 inclusive.
|
|
||||||
|
|
||||||
3. Specific values and ranges
|
|
||||||
|
|
||||||
```python
|
|
||||||
"rating": "int[1-5]"
|
|
||||||
"rating": "int[1,2,3,4-5]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integer values 1, 2, 3, 4, or 5.
|
|
||||||
|
|
||||||
4. Ranges with negative numbers:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"rating": "int[-100 - -90]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integer values from -100 to -90.
|
|
||||||
|
|
||||||
### Double (`double`)
|
|
||||||
|
|
||||||
Represents a floating-point number. Supports the same conditions as integers.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Basic double: `"double"`
|
|
||||||
- With conditions: `"double[conditions]"`
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
- `conditions`: A comma-separated list of conditions.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Basic double:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"price": "double"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts any floating-point number for the key `price`.
|
|
||||||
|
|
||||||
2. Double with conditions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"percentage": "double[>=0.0,<=100.0]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts double values between 0.0 and 100.0 inclusive.
|
|
||||||
|
|
||||||
### Boolean (`bool`)
|
|
||||||
|
|
||||||
Represents a boolean value (`True` or `False`).
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"isActive": "bool"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a boolean value for the key `isActive`.
|
|
||||||
|
|
||||||
### Array (`array`)
|
|
||||||
|
|
||||||
Represents a list of elements of a specified type. You can specify conditions on the length of the array.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Basic array: `"array[element_type]"`
|
|
||||||
- With length conditions: `"array[element_type,length_conditions]"`
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
- `element_type`: The type of the elements in the array.
|
|
||||||
- `length_conditions`: Conditions on the array length (same as integer conditions).
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Basic array:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"tags": "array[str]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a list of strings for the key `tags`.
|
|
||||||
|
|
||||||
2. Array with length conditions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"scores": "array[int[>=0,<=100],>=1,<=5]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a list of 1 to 5 integers between 0 and 100 inclusive.
|
|
||||||
|
|
||||||
3. Fixed-length array:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"coordinates": "array[double, 2]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a list of exactly 2 double values.
|
|
||||||
|
|
||||||
4. More complex restraints:
|
|
||||||
```python
|
|
||||||
"coordinates": "array[array[int[>0]] - tuple[1, 1]], 2]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tuple (`tuple`)
|
|
||||||
|
|
||||||
Represents a fixed-size sequence of elements of specified types.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"tuple[element_type1, element_type2]"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
- `element_typeN`: The type of the Nth element in the tuple.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Basic tuple:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"point": "tuple[int, int]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a tuple or list of two integers.
|
|
||||||
|
|
||||||
2. Tuple with mixed types:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"userInfo": "tuple[str, int, bool]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a tuple of a string, an integer, and a boolean.
|
|
||||||
|
|
||||||
### Nested Dictionaries
|
|
||||||
|
|
||||||
Represents a nested dictionary structure. Dictionaries are defined using Python's dictionary syntax `{}` in the type definitions.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"settings": {
|
|
||||||
"volume": "int[>=0,<=100]",
|
|
||||||
"brightness": "int[>=0,<=100]",
|
|
||||||
"mode": "str"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
- Define the expected keys and their types within the dictionary.
|
|
||||||
- You can use all the supported types for the values.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Nested dictionary:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"user": {
|
|
||||||
"name": "str",
|
|
||||||
"age": "int[>=0]",
|
|
||||||
"preferences": {
|
|
||||||
"theme": "str",
|
|
||||||
"notifications": "bool"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Defines a nested dictionary structure for the key `user`.
|
|
||||||
|
|
||||||
### Nil (`nil`)
|
|
||||||
|
|
||||||
Represents a `None` value.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"optionalValue": "int | nil"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
- Use `nil` to allow a value to be `None`.
|
|
||||||
- Often used with union types to specify optional values.
|
|
||||||
|
|
||||||
### Any (`any`)
|
|
||||||
|
|
||||||
Represents any value.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"metadata": "any"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
- Use `any` when any value is acceptable.
|
|
||||||
- Useful for keys where the value is not constrained.
|
|
||||||
|
|
||||||
### Type References (`@`)
|
|
||||||
|
|
||||||
Allows you to define reusable types and reference them.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Define a named type:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"@typeName": "type_definition"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Reference a named type:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"key": "@typeName"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Defining and using a named type:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"@positiveInt": "int[>0]"
|
|
||||||
"userId": "@positiveInt"
|
|
||||||
```
|
|
||||||
|
|
||||||
Defines a reusable type `@positiveInt` and uses it for the key `userId`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### Subtracting Domains (`-`)
|
|
||||||
|
|
||||||
Allows you to specify that a value should not match a certain type or condition.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"typeA - typeB"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
- The value must match `typeA` but not `typeB`.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Excluding certain strings:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"message": "str - str[.*error.*]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts any string that does not match the regex pattern `.*error.*`.
|
|
||||||
|
|
||||||
2. Excluding a range of numbers:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"score": "int[0-100] - int[>=90]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integers between 0 and 100, excluding values greater than or equal to 90.
|
|
||||||
|
|
||||||
3. Excluding multiple types:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"score": "int[>0,<100] - int[>90] - int[<10]"
|
|
||||||
# Union, then subtraction:
|
|
||||||
"score": "int[>0,<100] - int[>90] | int[<10]"
|
|
||||||
"score": "int[>0,<100] - (int[>90] | int[<10])" # same thing
|
|
||||||
# Use parenthesis to run subtraction first
|
|
||||||
"score": "int[>0,<50] | (int[<100] - int[<10])"
|
|
||||||
"score": "(int[<100] - int[<10]) | int[>0,<50]"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: Union is handled before subtraction.
|
|
||||||
|
|
||||||
4. Allowing all but a specific value:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"specialNumber": "any - int[0]"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Union Types (`|`)
|
|
||||||
|
|
||||||
Allows you to specify that a value can be one of multiple types.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
"typeA | typeB | typeC"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
- The value must match at least one of the specified types.
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Multiple possible types:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"data": "int | str | bool"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts an integer, string, or boolean value for the key `data`.
|
|
||||||
|
|
||||||
2. Combining with arrays:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"mixedList": "array[int | str]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts a list of integers or strings.
|
|
||||||
|
|
||||||
### Conditional Ranges and Values
|
|
||||||
|
|
||||||
Specifies conditions that values must satisfy, including ranges and specific values.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
- Greater than: `">value"`
|
|
||||||
- Less than: `"<value"`
|
|
||||||
- Greater than or equal to: `">=value"`
|
|
||||||
- Less than or equal to: `"<="value"`
|
|
||||||
- Range: `"start-end"`
|
|
||||||
- Specific values: `"value1,value2,value3"`
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
|
|
||||||
1. Integer conditions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"level": "int[>=1,<=10]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integers from 1 to 10 inclusive.
|
|
||||||
|
|
||||||
2. Double with range:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"latitude": "double[-90.0 - 90.0]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts doubles between -90.0 and 90.0 inclusive.
|
|
||||||
|
|
||||||
3. Specific values:
|
|
||||||
|
|
||||||
```python
|
|
||||||
"status": "int[1,2,3]"
|
|
||||||
```
|
|
||||||
|
|
||||||
Accepts integers that are either 1, 2, or 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
Exception --> JvvException
|
|
||||||
JvvException --> JvvRuntimeException
|
|
||||||
JvvException --> JvvSyntaxError
|
|
||||||
|
|
||||||
JvvRuntimeException --> UnknownProperty["UnknownProperty<br/><small>Raised when a key in config<br/>isn't defined in property types</small>"]
|
|
||||||
JvvRuntimeException --> InvalidPropertyType["InvalidPropertyType<br/><small>Raised when a value doesn't<br/>match its type definition</small>"]
|
|
||||||
InvalidPropertyType --> MissingRequiredKey["MissingRequiredKey<br/><small>Raised when a required key<br/>is missing from config</small>"]
|
|
||||||
MissingRequiredKey --> MissingGroupKey["MissingGroupKey<br/><small>Raised when some keys in a<br/>property group are missing</small>"]
|
|
||||||
|
|
||||||
JvvSyntaxError --> PropertySyntaxError["PropertySyntaxError<br/><small>Raised when property type<br/>definitions have syntax errors</small>"]
|
|
||||||
|
|
||||||
classDef base fill:#eee,stroke:#333,stroke-width:2px;
|
|
||||||
classDef jvv fill:#d4e6f1,stroke:#2874a6,stroke-width:2px;
|
|
||||||
classDef runtime fill:#d5f5e3,stroke:#196f3d,stroke-width:2px;
|
|
||||||
classDef syntax fill:#fdebd0,stroke:#b9770e,stroke-width:2px;
|
|
||||||
classDef error fill:#fadbd8,stroke:#943126,stroke-width:2px;
|
|
||||||
|
|
||||||
class Exception base;
|
|
||||||
class JvvException jvv;
|
|
||||||
class JvvRuntimeException,JvvSyntaxError runtime;
|
|
||||||
class PropertySyntaxError syntax;
|
|
||||||
class UnknownProperty,InvalidPropertyType,MissingRequiredKey,MissingGroupKey error;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Types
|
|
||||||
|
|
||||||
- **str**: Basic string type.
|
|
||||||
|
|
||||||
- Arguments:
|
|
||||||
- `regex_pattern` (optional): A regex pattern the string must match.
|
|
||||||
- Example: `"str[^[A-Za-z]+$]"`
|
|
||||||
|
|
||||||
- **int**: Integer type with conditions.
|
|
||||||
|
|
||||||
- Arguments:
|
|
||||||
- `conditions`: Inequalities (`>=`, `<=`, `>`, `<`), specific values (`value1,value2`), ranges (`start-end`).
|
|
||||||
- Example: `"int[>=0,<=100]"`
|
|
||||||
|
|
||||||
- **double**: Double (floating-point) type with conditions.
|
|
||||||
|
|
||||||
- Arguments:
|
|
||||||
- Same as `int`.
|
|
||||||
- Example: `"double[>0.0]"`
|
|
||||||
|
|
||||||
- **bool**: Boolean type.
|
|
||||||
|
|
||||||
- Arguments: None.
|
|
||||||
- Example: `"bool"`
|
|
||||||
|
|
||||||
- **array**: Array (list) of elements of a specified type.
|
|
||||||
|
|
||||||
- Arguments:
|
|
||||||
- `element_type`: Type of elements in the array.
|
|
||||||
- `length_conditions` (optional): Conditions on the array length.
|
|
||||||
- Example: `"array[int[>=0],>=1,<=10]"`
|
|
||||||
|
|
||||||
- **tuple**: Fixed-size sequence of elements of specified types.
|
|
||||||
|
|
||||||
- Arguments:
|
|
||||||
- List of element types.
|
|
||||||
- Example: `"tuple[str, int, bool]"`
|
|
||||||
|
|
||||||
- **nil**: Represents a `None` value.
|
|
||||||
|
|
||||||
- Arguments: None.
|
|
||||||
- Example: `"nil"`
|
|
||||||
|
|
||||||
- **any**: Accepts any value.
|
|
||||||
|
|
||||||
- Arguments: None.
|
|
||||||
- Example: `"any"`
|
|
||||||
|
|
||||||
- **Type References**: Reusable type definitions.
|
|
||||||
- Arguments:
|
|
||||||
- `@typeName`: Reference to a named type.
|
|
||||||
- Example:
|
|
||||||
- Define: `"@positiveInt": "int[>0]"`
|
|
||||||
- Use: `"userId": "@positiveInt"`
|
|
||||||
|
|
||||||
### Type Combinations
|
|
||||||
|
|
||||||
- **Union Types** (`|`): Value must match one of multiple types.
|
|
||||||
|
|
||||||
- Syntax: `"typeA | typeB"`
|
|
||||||
- Example: `"str | int"`
|
|
||||||
|
|
||||||
- **Subtracting Domains** (`-`): Value must match `typeA` but not `typeB`.
|
|
||||||
- Syntax: `"typeA - typeB"`
|
|
||||||
- Example: `"int - int[13]"` (any integer except 13)
|
|
||||||
|
|
||||||
### Escaping Characters
|
|
||||||
|
|
||||||
- `!`: Escapes commas, slashes, and other jsonvv characters within strings.
|
|
||||||
- `\`: Escapes within a regex pattern.
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
from .exceptions import (
|
|
||||||
InvalidPropertyType,
|
|
||||||
JvvException,
|
|
||||||
JvvRuntimeException,
|
|
||||||
JvvSyntaxError,
|
|
||||||
MissingRequiredKey,
|
|
||||||
PropertySyntaxError,
|
|
||||||
UnknownProperty,
|
|
||||||
)
|
|
||||||
from .validator import JsonValidator
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'JvvRuntimeException',
|
|
||||||
'JvvSyntaxError',
|
|
||||||
'PropertySyntaxError',
|
|
||||||
'JsonValidator',
|
|
||||||
'JvvException',
|
|
||||||
'InvalidPropertyType',
|
|
||||||
'UnknownProperty',
|
|
||||||
'MissingRequiredKey',
|
|
||||||
]
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from jsonvv.exceptions import InvalidPropertyType, JvvSyntaxError, UnknownProperty
|
|
||||||
from jsonvv.validator import JsonValidator
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(file_path: Path) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Load and parse a JSON file.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open(file_path) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise ValueError(f"Invalid JSON in {file_path}: {e}")
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise ValueError(f"File not found: {file_path}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""JSON Value Validator - Validate JSON data against a schema."""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="JSON Value Validator - Validate JSON data against a schema."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'properties_file', type=Path, help='JSON file containing the property type definitions'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-i', '--input', type=Path, help='JSON file containing the data to validate'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--check', action='store_true', help='Check if the properties file is valid'
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Load property types
|
|
||||||
property_types = load_json(args.properties_file)
|
|
||||||
validator = JsonValidator(property_types)
|
|
||||||
|
|
||||||
if args.check:
|
|
||||||
print("✓ Property types are valid")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not args.input:
|
|
||||||
parser.error("Either --input or --check must be specified")
|
|
||||||
|
|
||||||
# Load and validate data
|
|
||||||
data = load_json(args.input)
|
|
||||||
validator.validate(data)
|
|
||||||
print("✓ Data is valid")
|
|
||||||
|
|
||||||
except (InvalidPropertyType, UnknownProperty) as e:
|
|
||||||
print(f"Validation Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except JvvSyntaxError as e:
|
|
||||||
print(f"Syntax Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"File Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
"""Exception classes for jsonvv"""
|
|
||||||
|
|
||||||
|
|
||||||
class JvvException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class JvvRuntimeException(JvvException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class JvvSyntaxError(JvvException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownProperty(JvvRuntimeException, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPropertyType(JvvRuntimeException, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingRequiredKey(InvalidPropertyType):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingGroupKey(MissingRequiredKey):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PropertySyntaxError(JvvSyntaxError):
|
|
||||||
pass
|
|
||||||
|
|
@ -1,309 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from .exceptions import InvalidPropertyType
|
|
||||||
from .strings import string_validator
|
|
||||||
from .types import (
|
|
||||||
AnyType,
|
|
||||||
ArrayType,
|
|
||||||
BaseType,
|
|
||||||
BoolType,
|
|
||||||
DoubleType,
|
|
||||||
IntType,
|
|
||||||
NilType,
|
|
||||||
StringType,
|
|
||||||
SubtractionType,
|
|
||||||
TupleType,
|
|
||||||
Type,
|
|
||||||
UnionType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
|
||||||
def __init__(self, type_str: str):
|
|
||||||
self.type_str = type_str
|
|
||||||
self.pos = 0
|
|
||||||
self.length = len(type_str)
|
|
||||||
|
|
||||||
def parse(self) -> Type:
|
|
||||||
"""Main entry point"""
|
|
||||||
result = self.parse_subtraction() # Start with subtraction instead of union
|
|
||||||
self.skip_whitespace()
|
|
||||||
if self.pos < self.length:
|
|
||||||
raise RuntimeError(f"Unexpected character at position {self.pos}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_union(self) -> Type:
|
|
||||||
"""Handles type1 | type2 | type3"""
|
|
||||||
types = [self.parse_term()] # Parse first term
|
|
||||||
|
|
||||||
while self.pos < self.length:
|
|
||||||
self.skip_whitespace()
|
|
||||||
if not self.match('|'):
|
|
||||||
break
|
|
||||||
types.append(self.parse_term()) # Parse additional terms
|
|
||||||
|
|
||||||
return types[0] if len(types) == 1 else UnionType(types)
|
|
||||||
|
|
||||||
def parse_subtraction(self) -> Type:
|
|
||||||
"""Handles type1 - type2"""
|
|
||||||
left = self.parse_union() # Start with union
|
|
||||||
|
|
||||||
while self.pos < self.length:
|
|
||||||
self.skip_whitespace()
|
|
||||||
if not self.match('-'):
|
|
||||||
break
|
|
||||||
right = self.parse_union() # Parse right side as union
|
|
||||||
left = SubtractionType(left, right)
|
|
||||||
|
|
||||||
return left
|
|
||||||
|
|
||||||
def parse_term(self) -> Type:
|
|
||||||
"""Handles basic terms and parenthesized expressions"""
|
|
||||||
self.skip_whitespace()
|
|
||||||
|
|
||||||
if self.match('('):
|
|
||||||
type_obj = self.parse_subtraction() # Parse subtraction inside parens
|
|
||||||
if not self.match(')'):
|
|
||||||
raise RuntimeError("Unclosed parenthesis")
|
|
||||||
return type_obj
|
|
||||||
|
|
||||||
return self.parse_basic_type()
|
|
||||||
|
|
||||||
def parse_basic_type(self) -> Type:
|
|
||||||
"""Handles basic types with conditions"""
|
|
||||||
name = self.parse_identifier()
|
|
||||||
|
|
||||||
# Special handling for array type
|
|
||||||
if name == 'array':
|
|
||||||
return self.parse_array_type()
|
|
||||||
|
|
||||||
# Special handling for tuple type
|
|
||||||
if name == 'tuple':
|
|
||||||
# Don't advance position, let parse_tuple_type handle it
|
|
||||||
return self.parse_tuple_type()
|
|
||||||
|
|
||||||
conditions = None
|
|
||||||
self.skip_whitespace()
|
|
||||||
|
|
||||||
if self.match('['):
|
|
||||||
start = self.pos
|
|
||||||
# For all types, just capture everything until the closing bracket
|
|
||||||
bracket_count = 1 # Track nested brackets
|
|
||||||
while self.pos < self.length:
|
|
||||||
if self.type_str[self.pos] == '[':
|
|
||||||
bracket_count += 1
|
|
||||||
elif self.type_str[self.pos] == ']':
|
|
||||||
bracket_count -= 1
|
|
||||||
if bracket_count == 0:
|
|
||||||
break
|
|
||||||
self.pos += 1
|
|
||||||
|
|
||||||
if bracket_count > 0:
|
|
||||||
raise RuntimeError("Unclosed '['")
|
|
||||||
conditions = self.type_str[start : self.pos]
|
|
||||||
|
|
||||||
if not self.match(']'):
|
|
||||||
raise RuntimeError("Expected ']'")
|
|
||||||
|
|
||||||
# Return appropriate type based on name
|
|
||||||
if name == 'str':
|
|
||||||
return StringType(conditions)
|
|
||||||
elif name == 'int':
|
|
||||||
return IntType(conditions)
|
|
||||||
elif name == 'double':
|
|
||||||
return DoubleType(conditions)
|
|
||||||
elif name == 'bool':
|
|
||||||
return BoolType()
|
|
||||||
elif name == 'any':
|
|
||||||
return AnyType()
|
|
||||||
elif name == 'nil':
|
|
||||||
return NilType() # Add this type
|
|
||||||
elif name == 'tuple':
|
|
||||||
return self.parse_tuple_type()
|
|
||||||
elif name.startswith('@'):
|
|
||||||
return ReferenceType(name[1:])
|
|
||||||
return BaseType(name, conditions)
|
|
||||||
|
|
||||||
def peek(self, char: str) -> bool:
|
|
||||||
"""Looks ahead for a character without advancing position"""
|
|
||||||
self.skip_whitespace()
|
|
||||||
return self.pos < self.length and self.type_str[self.pos] == char
|
|
||||||
|
|
||||||
def parse_array_type(self) -> Type:
|
|
||||||
"""Handles array[type, length?]"""
|
|
||||||
if not self.match('['):
|
|
||||||
return ArrayType(AnyType(), None) # Default array type
|
|
||||||
|
|
||||||
# Parse the element type (which could be a complex type)
|
|
||||||
element_type = self.parse_subtraction() # Start with subtraction to handle all cases
|
|
||||||
|
|
||||||
length_conditions = None
|
|
||||||
self.skip_whitespace()
|
|
||||||
|
|
||||||
# Check for length conditions after comma
|
|
||||||
if self.match(','):
|
|
||||||
self.skip_whitespace()
|
|
||||||
start = self.pos
|
|
||||||
while self.pos < self.length and self.type_str[self.pos] != ']':
|
|
||||||
self.pos += 1
|
|
||||||
if self.pos >= self.length:
|
|
||||||
raise RuntimeError("Unclosed array type")
|
|
||||||
length_conditions = self.type_str[start : self.pos].strip()
|
|
||||||
|
|
||||||
if not self.match(']'):
|
|
||||||
raise RuntimeError("Expected ']' in array type")
|
|
||||||
|
|
||||||
return ArrayType(element_type, length_conditions)
|
|
||||||
|
|
||||||
def parse_tuple_type(self) -> Type:
|
|
||||||
"""Handles tuple[type1, type2, ...]"""
|
|
||||||
|
|
||||||
if not self.match('['):
|
|
||||||
raise RuntimeError("Expected '[' after 'tuple'")
|
|
||||||
|
|
||||||
types = []
|
|
||||||
while True:
|
|
||||||
self.skip_whitespace()
|
|
||||||
if self.match(']'):
|
|
||||||
break
|
|
||||||
|
|
||||||
# Parse complex type expressions within tuple arguments
|
|
||||||
type_obj = self.parse_subtraction() # Start with subtraction to handle all operations
|
|
||||||
types.append(type_obj)
|
|
||||||
|
|
||||||
self.skip_whitespace()
|
|
||||||
if not self.match(','):
|
|
||||||
if self.match(']'):
|
|
||||||
break
|
|
||||||
raise RuntimeError("Expected ',' or ']' in tuple type")
|
|
||||||
|
|
||||||
return TupleType(types)
|
|
||||||
|
|
||||||
def parse_identifier(self) -> str:
|
|
||||||
"""Parses an identifier"""
|
|
||||||
self.skip_whitespace()
|
|
||||||
start = self.pos
|
|
||||||
|
|
||||||
# Only consume alphanumeric and underscore characters
|
|
||||||
while self.pos < self.length and (
|
|
||||||
self.type_str[self.pos].isalnum() or self.type_str[self.pos] in '_.@!'
|
|
||||||
):
|
|
||||||
self.pos += 1
|
|
||||||
|
|
||||||
if start == self.pos:
|
|
||||||
raise RuntimeError(f'Expected identifier at position {self.pos}')
|
|
||||||
|
|
||||||
result = self.type_str[start : self.pos]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def skip_whitespace(self) -> None:
|
|
||||||
"""Skips whitespace characters"""
|
|
||||||
while self.pos < self.length and self.type_str[self.pos].isspace():
|
|
||||||
self.pos += 1
|
|
||||||
|
|
||||||
def match(self, char: str) -> bool:
|
|
||||||
"""Tries to match a character, advances position if matched"""
|
|
||||||
self.skip_whitespace()
|
|
||||||
if self.pos < self.length and self.type_str[self.pos] == char:
|
|
||||||
self.pos += 1
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def peek_word(self, word: str) -> bool:
|
|
||||||
"""Looks ahead for a word without advancing position"""
|
|
||||||
self.skip_whitespace()
|
|
||||||
return (
|
|
||||||
self.pos + len(word) <= self.length
|
|
||||||
and self.type_str[self.pos : self.pos + len(word)] == word
|
|
||||||
and (
|
|
||||||
self.pos + len(word) == self.length
|
|
||||||
or not self.type_str[self.pos + len(word)].isalnum()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Python's import system is a pain,
|
|
||||||
so I'm moving DictType and ReferenceType here.
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DictType(Type):
|
|
||||||
type_dict: Dict[str, Any]
|
|
||||||
type_registry: Dict[str, Any]
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise InvalidPropertyType(f"Expected dict at {'.'.join(path)}, got {type(value)}")
|
|
||||||
|
|
||||||
# Track matched patterns and required keys
|
|
||||||
any_pattern_matched = False
|
|
||||||
required_patterns = {
|
|
||||||
pattern[1:]: False for pattern in self.type_dict if pattern.startswith('*')
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, val in value.items():
|
|
||||||
pattern_matched = False
|
|
||||||
for pattern, type_def in self.type_dict.items():
|
|
||||||
# Strip * for required patterns when matching
|
|
||||||
match_pattern = pattern[1:] if pattern.startswith('*') else pattern
|
|
||||||
|
|
||||||
if string_validator(key, match_pattern):
|
|
||||||
pattern_matched = True
|
|
||||||
any_pattern_matched = True
|
|
||||||
|
|
||||||
# Mark required pattern as found
|
|
||||||
if pattern.startswith('*'):
|
|
||||||
required_patterns[match_pattern] = True
|
|
||||||
|
|
||||||
# Parse the type definition string into a Type object
|
|
||||||
expected_type = parse_type_def(type_def, type_registry)
|
|
||||||
expected_type.validate(val, path + [key], type_registry)
|
|
||||||
|
|
||||||
if not pattern_matched:
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Key {key} at {'.'.join(path)} does not match any allowed patterns"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if all required patterns were matched
|
|
||||||
missing_required = [pattern for pattern, found in required_patterns.items() if not found]
|
|
||||||
if missing_required:
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Missing required properties matching patterns: {', '.join(missing_required)} at {'.'.join(path)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not any_pattern_matched:
|
|
||||||
raise InvalidPropertyType(f"No properties at {'.'.join(path)} matched any patterns")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReferenceType(Type):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if self.name not in type_registry:
|
|
||||||
raise RuntimeError(f"Unknown type reference: @{self.name}")
|
|
||||||
|
|
||||||
ref_type = type_registry[self.name]
|
|
||||||
|
|
||||||
if isinstance(ref_type, dict):
|
|
||||||
# Create a DictType for dictionary references
|
|
||||||
dict_type = DictType(ref_type, type_registry)
|
|
||||||
dict_type.validate(value, path, type_registry)
|
|
||||||
else:
|
|
||||||
# For non-dictionary types
|
|
||||||
ref_type.validate(value, path, type_registry)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"@{self.name}"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_type_def(type_def: Any, type_registry: Dict[str, Type]) -> Type:
|
|
||||||
if isinstance(type_def, str):
|
|
||||||
parser = Parser(type_def)
|
|
||||||
return parser.parse()
|
|
||||||
elif isinstance(type_def, dict):
|
|
||||||
return DictType(type_def, type_registry)
|
|
||||||
raise InvalidPropertyType(f"Invalid type definition: {type_def}")
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import re
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
class StringValidator:
|
|
||||||
def __init__(self, pattern: str):
|
|
||||||
self.pattern = pattern
|
|
||||||
self.patterns = self._split_patterns(pattern)
|
|
||||||
|
|
||||||
def _split_patterns(self, p: str) -> List[str]:
|
|
||||||
patterns = []
|
|
||||||
current = []
|
|
||||||
in_regex = False
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
while i < len(p):
|
|
||||||
if p[i] == '/' and (i == 0 or p[i - 1] != '!'):
|
|
||||||
in_regex = not in_regex
|
|
||||||
current.append(p[i])
|
|
||||||
elif p[i] == ',' and not in_regex:
|
|
||||||
# Check if comma is escaped
|
|
||||||
if i > 0 and p[i - 1] == '!':
|
|
||||||
current.append(',')
|
|
||||||
else:
|
|
||||||
# End of pattern
|
|
||||||
patterns.append(''.join(current))
|
|
||||||
current = []
|
|
||||||
else:
|
|
||||||
current.append(p[i])
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if current:
|
|
||||||
patterns.append(''.join(current))
|
|
||||||
|
|
||||||
result = [p.strip() for p in patterns if p.strip()]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _is_regex_pattern(self, p: str) -> bool:
|
|
||||||
is_regex = p.startswith('/') and p.endswith('/') and not p.endswith('!/')
|
|
||||||
return is_regex
|
|
||||||
|
|
||||||
def _clean_literal_pattern(self, p: str) -> str:
|
|
||||||
return re.sub(r'!(.)', r'\1', p)
|
|
||||||
|
|
||||||
def validate(self, value: str) -> bool:
|
|
||||||
for p in self.patterns:
|
|
||||||
p = self._clean_literal_pattern(p)
|
|
||||||
if self._is_regex_pattern(p):
|
|
||||||
regex = p[1:-1]
|
|
||||||
match = bool(re.match(regex, value))
|
|
||||||
if match:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
match = value == p
|
|
||||||
if match:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def string_validator(value: str, pattern: str) -> bool:
|
|
||||||
validator = StringValidator(pattern)
|
|
||||||
result = validator.validate(value)
|
|
||||||
return result
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict, List, Optional, Union
|
|
||||||
|
|
||||||
from .exceptions import InvalidPropertyType
|
|
||||||
from .strings import string_validator
|
|
||||||
|
|
||||||
TYPE_NAMES = {'array', 'tuple', 'str', 'int', 'double', 'bool', 'any', 'nil', 'tuple'}
|
|
||||||
|
|
||||||
|
|
||||||
class Type(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, 'Type']) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BaseType(Type):
|
|
||||||
"""Base class for all types"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
conditions: Optional[str] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
# Raise error early
|
|
||||||
if not self.name.startswith('@') and self.name not in TYPE_NAMES:
|
|
||||||
raise InvalidPropertyType(f'Unknown base type {self.name}')
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if self.name in type_registry:
|
|
||||||
type_registry[self.name].validate(value, path, type_registry)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f'Unknown base type {self.name}')
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NilType(Type):
|
|
||||||
"""Represents a nil/null type"""
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if value is not None:
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected nil, got {value}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "nil"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StringType(Type):
|
|
||||||
pattern: Optional[str] = None
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected string, got {type(value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.pattern:
|
|
||||||
if not string_validator(value, self.pattern):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: {value} does not match pattern '{self.pattern}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"str[{self.pattern}]" if self.pattern else "str"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class NumericalType(Type):
|
|
||||||
conditions: Optional[str] = None
|
|
||||||
numeric_type: Type = float # Default to float
|
|
||||||
type_name: str = "number" # For error messages
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
allowed_types = (int, float) if self.numeric_type is float else (int,)
|
|
||||||
if not isinstance(value, allowed_types):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected {self.type_name}, got {type(value).__name__}"
|
|
||||||
)
|
|
||||||
if self.conditions and not self._check_conditions(self.numeric_type(value)):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: {value} does not match conditions '{self.conditions}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _check_conditions(self, value: Union[int, float]) -> bool:
|
|
||||||
if not self.conditions:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Split by comma and handle each condition
|
|
||||||
conditions = [c.strip() for c in self.conditions.split(',')]
|
|
||||||
|
|
||||||
for condition in conditions:
|
|
||||||
try:
|
|
||||||
# Handle comparisons
|
|
||||||
if '>=' in condition:
|
|
||||||
if value >= self.numeric_type(condition.replace('>=', '')):
|
|
||||||
return True
|
|
||||||
elif '<=' in condition:
|
|
||||||
if value <= self.numeric_type(condition.replace('<=', '')):
|
|
||||||
return True
|
|
||||||
elif '>' in condition:
|
|
||||||
if value > self.numeric_type(condition.replace('>', '')):
|
|
||||||
return True
|
|
||||||
elif '<' in condition:
|
|
||||||
if value < self.numeric_type(condition.replace('<', '')):
|
|
||||||
return True
|
|
||||||
# Handle ranges (e.g., "1.5-5.5")
|
|
||||||
elif '-' in condition[1:]:
|
|
||||||
# split by the -, ignoring the first character
|
|
||||||
range_s, range_e = condition[1:].split('-', 1)
|
|
||||||
range_s = self.numeric_type(condition[0] + range_s)
|
|
||||||
range_e = self.numeric_type(range_e)
|
|
||||||
if range_s <= value <= range_e:
|
|
||||||
return True
|
|
||||||
# Handle single values
|
|
||||||
else:
|
|
||||||
if value == self.numeric_type(condition):
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.type_name}[{self.conditions}]" if self.conditions else self.type_name
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class IntType(NumericalType):
|
|
||||||
def __init__(self, conditions: Optional[str] = None):
|
|
||||||
super().__init__(conditions=conditions, numeric_type=int, type_name="int")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DoubleType(NumericalType):
|
|
||||||
def __init__(self, conditions: Optional[str] = None):
|
|
||||||
super().__init__(conditions=conditions, numeric_type=float, type_name="double")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AnyType(Type):
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
# Any type accepts all values
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "any"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BoolType(Type):
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if not isinstance(value, bool):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected bool, got {type(value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ArrayType(Type):
|
|
||||||
element_type: Type
|
|
||||||
length_conditions: Optional[str] = None
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if not isinstance(value, list):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected array, got {type(value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.length_conditions:
|
|
||||||
array_len = len(value)
|
|
||||||
length_validator = IntType(self.length_conditions)
|
|
||||||
try:
|
|
||||||
length_validator._check_conditions(array_len)
|
|
||||||
except Exception:
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid array length at {'.'.join(path)}: got length {array_len}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, item in enumerate(value):
|
|
||||||
self.element_type.validate(item, path + [str(i)], type_registry)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TupleType(Type):
|
|
||||||
element_types: List[Type]
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
if not isinstance(value, (list, tuple)):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: expected tuple, got {type(value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(value) != len(self.element_types):
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid tuple length at {'.'.join(path)}: expected {len(self.element_types)}, got {len(value)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, (item, expected_type) in enumerate(zip(value, self.element_types)):
|
|
||||||
expected_type.validate(item, path + [str(i)], type_registry)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnionType(Type):
|
|
||||||
types: List[Type]
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
errors = []
|
|
||||||
for t in self.types:
|
|
||||||
try:
|
|
||||||
t.validate(value, path, type_registry)
|
|
||||||
return # If any type validates successfully, we're done
|
|
||||||
except InvalidPropertyType as e:
|
|
||||||
errors.append(str(e))
|
|
||||||
|
|
||||||
# If we get here, none of the types validated
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {'.'.join(path)}: {value} does not match any of the allowed types"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"({' | '.join(str(t) for t in self.types)})"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SubtractionType(Type):
|
|
||||||
base_type: Type
|
|
||||||
subtracted_type: Type
|
|
||||||
|
|
||||||
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
||||||
path_str = '.'.join(path)
|
|
||||||
|
|
||||||
# First check if value matches base type
|
|
||||||
matches_base = True
|
|
||||||
try:
|
|
||||||
self.base_type.validate(value, path, type_registry)
|
|
||||||
except InvalidPropertyType:
|
|
||||||
matches_base = False
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Then check if value matches subtracted type
|
|
||||||
matches_subtracted = True
|
|
||||||
try:
|
|
||||||
self.subtracted_type.validate(value, path, type_registry)
|
|
||||||
matches_subtracted = True
|
|
||||||
except InvalidPropertyType:
|
|
||||||
matches_subtracted = False
|
|
||||||
|
|
||||||
# Final validation decision
|
|
||||||
if matches_base and matches_subtracted:
|
|
||||||
raise InvalidPropertyType(f"Invalid value at {path_str}: {value} matches excluded type")
|
|
||||||
elif matches_base and not matches_subtracted:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
raise InvalidPropertyType(
|
|
||||||
f"Invalid value at {path_str}: {value} does not match base type"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"({self.base_type} - {self.subtracted_type})"
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from .exceptions import (
|
|
||||||
MissingGroupKey,
|
|
||||||
MissingRequiredKey,
|
|
||||||
PropertySyntaxError,
|
|
||||||
UnknownProperty,
|
|
||||||
)
|
|
||||||
from .parser import parse_type_def
|
|
||||||
from .strings import string_validator
|
|
||||||
from .types import Type
|
|
||||||
|
|
||||||
|
|
||||||
class JsonValidator:
|
|
||||||
def __init__(self, property_types):
|
|
||||||
self.property_types = property_types
|
|
||||||
# Create a registry for reference types and parsed type definitions
|
|
||||||
self.type_registry = {}
|
|
||||||
self.parsed_types = {}
|
|
||||||
# Track property groups
|
|
||||||
self.groups: Dict[str, List[str]] = {}
|
|
||||||
# Validate and pre-parse all type definitions
|
|
||||||
self.parse_types(property_types)
|
|
||||||
|
|
||||||
def validate(self, config_map):
|
|
||||||
# First validate groups
|
|
||||||
self.validate_groups(config_map)
|
|
||||||
# Then validate the rest
|
|
||||||
validate_config(config_map, self.property_types, self.type_registry, self.parsed_types)
|
|
||||||
|
|
||||||
def parse_types(self, property_types: Dict[str, Any], path: str = ""):
|
|
||||||
"""Validates and pre-parses all type definitions."""
|
|
||||||
for key, value in property_types.items():
|
|
||||||
current_path = f"{path}.{key}" if path else key
|
|
||||||
|
|
||||||
# Register reference types
|
|
||||||
if key.startswith('@'):
|
|
||||||
if len(key) == 1:
|
|
||||||
raise PropertySyntaxError(
|
|
||||||
f"Invalid key '{current_path}': '@' must be followed by a reference name"
|
|
||||||
)
|
|
||||||
self.type_registry[key[1:]] = value
|
|
||||||
|
|
||||||
# Validate key syntax for required properties
|
|
||||||
if key.startswith('*') and len(key) == 1:
|
|
||||||
raise PropertySyntaxError(
|
|
||||||
f"Invalid key '{current_path}': '*' must be followed by a property name"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register group dependencies
|
|
||||||
orig_key: Optional[str] = None
|
|
||||||
while (idx := key.rfind('$')) != -1:
|
|
||||||
# Get the original key before all $
|
|
||||||
if orig_key is None:
|
|
||||||
orig_key = key.split('$', 1)[0]
|
|
||||||
# Add to group registry
|
|
||||||
key, group = key[:idx], key[idx + 1 :]
|
|
||||||
if group not in self.groups:
|
|
||||||
self.groups[group] = []
|
|
||||||
self.groups[group].append(orig_key)
|
|
||||||
|
|
||||||
if isinstance(value, dict):
|
|
||||||
# Recursively validate and parse nested dictionaries
|
|
||||||
self.parse_types(value, current_path)
|
|
||||||
elif isinstance(value, str):
|
|
||||||
try:
|
|
||||||
# Pre-parse the type definition and store it
|
|
||||||
self.parsed_types[current_path] = parse_type_def(value, self.type_registry)
|
|
||||||
except Exception as e:
|
|
||||||
raise PropertySyntaxError(
|
|
||||||
f"Invalid type definition for '{current_path}': {str(e)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise PropertySyntaxError(
|
|
||||||
f"Invalid type definition for '{current_path}': must be a string or dictionary"
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_groups(self, config_map: Dict[str, Any]) -> None:
|
|
||||||
"""Validates that grouped properties are all present or all absent."""
|
|
||||||
group_presence: Dict[str, bool] = {}
|
|
||||||
|
|
||||||
# Check which groups have any properties present
|
|
||||||
for group, props in self.groups.items():
|
|
||||||
group_presence[group] = any(prop in config_map for prop in props)
|
|
||||||
|
|
||||||
# Validate group completeness
|
|
||||||
for group, is_present in group_presence.items():
|
|
||||||
props = self.groups[group]
|
|
||||||
if is_present:
|
|
||||||
# If any property in group exists, all must exist
|
|
||||||
missing = [prop for prop in props if prop not in config_map]
|
|
||||||
if missing:
|
|
||||||
raise MissingGroupKey(
|
|
||||||
f"Incomplete property group ${group}: missing {', '.join(missing)}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# If no property in group exists, none should exist
|
|
||||||
present = [prop for prop in props if prop in config_map]
|
|
||||||
if present:
|
|
||||||
raise MissingGroupKey(
|
|
||||||
f"Incomplete property group ${group}: found {', '.join(present)} but missing {', '.join(set(props) - set(present))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_config(
|
|
||||||
config_map: Dict[str, Any],
|
|
||||||
property_types: Dict[str, Any],
|
|
||||||
type_registry: Dict[str, Type],
|
|
||||||
parsed_types: Dict[str, Type],
|
|
||||||
parent_registry: Dict[str, Type] = None,
|
|
||||||
path: str = "",
|
|
||||||
) -> None:
|
|
||||||
"""Validates a configuration map against property types."""
|
|
||||||
|
|
||||||
# Create a new registry for this scope, inheriting from parent if it exists
|
|
||||||
local_registry = dict(parent_registry or type_registry)
|
|
||||||
|
|
||||||
# Track required properties
|
|
||||||
required_props = {key[1:]: False for key in property_types if key.startswith('*')}
|
|
||||||
|
|
||||||
# Validate each property in config
|
|
||||||
for key, value in config_map.items():
|
|
||||||
type_def = None
|
|
||||||
current_path = f"{path}.{key}" if path else key
|
|
||||||
|
|
||||||
# Strip group suffix for type lookup
|
|
||||||
lookup_key = key.split('$')[0] if '$' in key else key
|
|
||||||
|
|
||||||
if lookup_key in property_types:
|
|
||||||
type_def = property_types[lookup_key]
|
|
||||||
|
|
||||||
# If the value is a dict and type_def is also a dict, recurse with new scope
|
|
||||||
if isinstance(value, dict) and isinstance(type_def, dict):
|
|
||||||
validate_config(
|
|
||||||
value, type_def, type_registry, parsed_types, local_registry, current_path
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif '*' + lookup_key in property_types:
|
|
||||||
type_def = property_types['*' + lookup_key]
|
|
||||||
required_props[lookup_key] = True
|
|
||||||
else:
|
|
||||||
# Check pattern matches
|
|
||||||
for pattern, pattern_type in property_types.items():
|
|
||||||
if pattern.startswith('@') or pattern.startswith('*'):
|
|
||||||
continue
|
|
||||||
pattern_base = pattern.split('$')[0] if '$' in pattern else pattern
|
|
||||||
if string_validator(lookup_key, pattern_base):
|
|
||||||
type_def = pattern_type
|
|
||||||
current_path = f"{path}.{pattern}" if path else pattern
|
|
||||||
break
|
|
||||||
|
|
||||||
if type_def is None:
|
|
||||||
raise UnknownProperty(f"Unknown property: {key}")
|
|
||||||
|
|
||||||
# Use pre-parsed type if available, otherwise parse it
|
|
||||||
expected_type = parsed_types.get(current_path)
|
|
||||||
if expected_type is None:
|
|
||||||
expected_type = parse_type_def(type_def, local_registry)
|
|
||||||
expected_type.validate(value, [key], local_registry)
|
|
||||||
|
|
||||||
# Check for missing required properties
|
|
||||||
missing_required = [key for key, found in required_props.items() if not found]
|
|
||||||
if missing_required:
|
|
||||||
raise MissingRequiredKey(f"Missing required properties: {', '.join(missing_required)}")
|
|
||||||
|
|
||||||
# Check for missing required properties
|
|
||||||
missing_required = [key for key, found in required_props.items() if not found]
|
|
||||||
if missing_required:
|
|
||||||
raise MissingRequiredKey(f"Missing required properties: {', '.join(missing_required)}")
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
rm -rf ./dist
|
|
||||||
|
|
||||||
vermin . --eval-annotations --target=3.8 --violations jsonvv/ || exit 1
|
|
||||||
|
|
||||||
python -m build
|
|
||||||
twine check dist/*
|
|
||||||
|
|
||||||
read -p "Confirm publish? (y/n) " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
twine upload dist/*
|
|
||||||
fi
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=1.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
name = "jsonvv"
|
|
||||||
version = "0.2.2"
|
|
||||||
description = "JSON value validator"
|
|
||||||
authors = ["daijro <daijro.dev@gmail.com>"]
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/daijro/camoufox"
|
|
||||||
homepage = "https://github.com/daijro/camoufox/tree/main/pythonlib/jsonvv"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = [
|
|
||||||
"json",
|
|
||||||
"validator",
|
|
||||||
"validation",
|
|
||||||
"typing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.8"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
|
||||||
jsonvv = "jsonvv.__main__:main"
|
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
{
|
|
||||||
"navigator.userAgent$__UA": "str",
|
|
||||||
"navigator.appVersion$__UA": "str",
|
|
||||||
"navigator.platform$__UA": "str",
|
|
||||||
"navigator.oscpu$__UA": "str",
|
|
||||||
|
|
||||||
"navigator.appCodeName$__PROD_CODE": "str",
|
|
||||||
"navigator.appName$__PROD_CODE": "str",
|
|
||||||
"navigator.product$__PROD_CODE": "str",
|
|
||||||
"navigator.productSub": "str[/^\\d+$/]",
|
|
||||||
"navigator.buildID": "str[/^\\d+$/]",
|
|
||||||
|
|
||||||
"screen.height$__SC": "int[>0]",
|
|
||||||
"screen.width$__SC": "int[>0]",
|
|
||||||
"screen.availHeight$__SC": "int[>=0]",
|
|
||||||
"screen.availWidth$__SC": "int[>=0]",
|
|
||||||
"screen.availTop": "int[>=0]",
|
|
||||||
"screen.availLeft": "int[>=0]",
|
|
||||||
"locale:language$__LOCALE": "str",
|
|
||||||
"locale:region$__LOCALE": "str",
|
|
||||||
"locale:script": "str",
|
|
||||||
"geolocation:latitude$__GEO": "double[-90 - 90]",
|
|
||||||
"geolocation:longitude$__GEO": "double[-180 - 180]",
|
|
||||||
"geolocation:accuracy": "double[>=0]",
|
|
||||||
"timezone": "str[/^[\\w_]+/[\\w_]+$/]",
|
|
||||||
|
|
||||||
"locale:all": "str",
|
|
||||||
"headers.Accept-Language": "str",
|
|
||||||
"navigator.language": "str",
|
|
||||||
"navigator.languages": "array[str]",
|
|
||||||
|
|
||||||
"headers.User-Agent": "str",
|
|
||||||
"headers.Accept-Encoding": "str",
|
|
||||||
"navigator.doNotTrack": "str[0, 1, unspecified]",
|
|
||||||
"navigator.hardwareConcurrency": "int[>0]",
|
|
||||||
"navigator.maxTouchPoints": "int[>=0]",
|
|
||||||
"navigator.cookieEnabled": "bool",
|
|
||||||
"navigator.globalPrivacyControl": "bool",
|
|
||||||
"navigator.onLine": "bool",
|
|
||||||
"window.history.length": "int[>=0]",
|
|
||||||
"pdfViewerEnabled": "bool",
|
|
||||||
|
|
||||||
"window.outerHeight$__W_OUTER": "int[>0]",
|
|
||||||
"window.outerWidth$__W_OUTER": "int[>0]",
|
|
||||||
"window.innerHeight$__W_INNER": "int[>0]",
|
|
||||||
"window.innerWidth$__W_INNER": "int[>0]",
|
|
||||||
|
|
||||||
"screen.colorDepth": "int[>0]",
|
|
||||||
"screen.pixelDepth": "int[>0]",
|
|
||||||
"screen.pageXOffset": "double",
|
|
||||||
"screen.pageYOffset": "double",
|
|
||||||
"window.scrollMinX": "int",
|
|
||||||
"window.scrollMinY": "int",
|
|
||||||
"window.scrollMaxX": "int",
|
|
||||||
"window.scrollMaxY": "int",
|
|
||||||
"window.screenX": "int",
|
|
||||||
"window.screenY": "int",
|
|
||||||
"window.devicePixelRatio": "double[>0]",
|
|
||||||
|
|
||||||
"document.body.clientWidth$__DOC_BODY": "int[>=0]",
|
|
||||||
"document.body.clientHeight$__DOC_BODY": "int[>=0]",
|
|
||||||
"document.body.clientTop": "int",
|
|
||||||
"document.body.clientLeft": "int",
|
|
||||||
|
|
||||||
"webrtc:ipv4": "@IPV4",
|
|
||||||
"webrtc:ipv6": "@IPV6",
|
|
||||||
"webrtc:localipv4": "@IPV4",
|
|
||||||
"webrtc:localipv6": "@IPV6",
|
|
||||||
|
|
||||||
"@IPV4": "str[/^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/]",
|
|
||||||
"@IPV6": "str[/^(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4})$/]",
|
|
||||||
|
|
||||||
"battery:charging$__BATTERY": "bool",
|
|
||||||
"battery:chargingTime$__BATTERY": "double[>=0]",
|
|
||||||
"battery:dischargingTime$__BATTERY": "double[>=0]",
|
|
||||||
"battery:level$__BATTERY": "double[>0]",
|
|
||||||
|
|
||||||
"fonts": "array[str]",
|
|
||||||
"fonts:spacing_seed": "int[>=0]",
|
|
||||||
|
|
||||||
"AudioContext:sampleRate": "int[>=0]",
|
|
||||||
"AudioContext:outputLatency": "double[>=0]",
|
|
||||||
"AudioContext:maxChannelCount": "int[>=0]",
|
|
||||||
|
|
||||||
"mediaDevices:micros": "int[>=0]",
|
|
||||||
"mediaDevices:webcams": "int[>=0]",
|
|
||||||
"mediaDevices:speakers": "int[>=0]",
|
|
||||||
"mediaDevices:enabled": "bool",
|
|
||||||
|
|
||||||
"webGl:renderer$__WEBGL": "str",
|
|
||||||
"webGl:vendor$__WEBGL": "str",
|
|
||||||
|
|
||||||
"webGl:supportedExtensions": "array[str[/^[\\w_]+$/]]",
|
|
||||||
"webGl2:supportedExtensions": "array[str[/^[\\w_]+$/]]",
|
|
||||||
|
|
||||||
"webGl:parameters": "@WEBGL_PARAMS",
|
|
||||||
"webGl2:parameters": "@WEBGL_PARAMS",
|
|
||||||
"webGl:parameters:blockIfNotDefined": "bool",
|
|
||||||
"webGl2:parameters:blockIfNotDefined": "bool",
|
|
||||||
|
|
||||||
"webGl:shaderPrecisionFormats": "@WEBGL_SHADER_PRECISION_FORMATS",
|
|
||||||
"webGl2:shaderPrecisionFormats": "@WEBGL_SHADER_PRECISION_FORMATS",
|
|
||||||
"webGl:shaderPrecisionFormats:blockIfNotDefined": "bool",
|
|
||||||
"webGl2:shaderPrecisionFormats:blockIfNotDefined": "bool",
|
|
||||||
|
|
||||||
"webGl:contextAttributes": "@WEBGL_CONTEXT_ATTRIBUTES",
|
|
||||||
"webGl2:contextAttributes": "@WEBGL_CONTEXT_ATTRIBUTES",
|
|
||||||
|
|
||||||
"@WEBGL_PARAMS": {
|
|
||||||
"2849": "int",
|
|
||||||
"2884": "bool",
|
|
||||||
"2885": "int",
|
|
||||||
"2886": "int",
|
|
||||||
"2928": "array[int, 2]",
|
|
||||||
"2929": "bool",
|
|
||||||
"2930": "bool",
|
|
||||||
"2931": "int",
|
|
||||||
"2932": "int",
|
|
||||||
"2960": "bool",
|
|
||||||
"2961": "int",
|
|
||||||
"2962": "int",
|
|
||||||
"2963": "int",
|
|
||||||
"2964": "int",
|
|
||||||
"2965": "int",
|
|
||||||
"2966": "int",
|
|
||||||
"2967": "int",
|
|
||||||
"2968": "int",
|
|
||||||
"2978": "array[int, 4]",
|
|
||||||
"3024": "bool",
|
|
||||||
"3042": "bool",
|
|
||||||
"3074": "int | nil",
|
|
||||||
"3088": "array[int, 4]",
|
|
||||||
"3089": "bool",
|
|
||||||
"3106": "array[int, 4]",
|
|
||||||
"3107": "array[bool, 4]",
|
|
||||||
"3314": "int | nil",
|
|
||||||
"3315": "int | nil",
|
|
||||||
"3316": "int | nil",
|
|
||||||
"3317": "int",
|
|
||||||
"3330": "int | nil",
|
|
||||||
"3331": "int | nil",
|
|
||||||
"3332": "int | nil",
|
|
||||||
"3333": "int",
|
|
||||||
"3379": "int",
|
|
||||||
"3386": "array[int, 2]",
|
|
||||||
"3408": "int",
|
|
||||||
"3410": "int",
|
|
||||||
"3411": "int",
|
|
||||||
"3412": "int",
|
|
||||||
"3413": "int",
|
|
||||||
"3414": "int",
|
|
||||||
"3415": "int",
|
|
||||||
"7936": "str",
|
|
||||||
"7937": "str",
|
|
||||||
"7938": "str",
|
|
||||||
"10752": "int",
|
|
||||||
"32773": "array[int, 4]",
|
|
||||||
"32777": "int",
|
|
||||||
"32823": "bool",
|
|
||||||
"32824": "int",
|
|
||||||
"32873": "nil",
|
|
||||||
"32877": "int | nil",
|
|
||||||
"32878": "int | nil",
|
|
||||||
"32883": "int | nil",
|
|
||||||
"32926": "bool",
|
|
||||||
"32928": "bool",
|
|
||||||
"32936": "int",
|
|
||||||
"32937": "int",
|
|
||||||
"32938": "int",
|
|
||||||
"32939": "bool",
|
|
||||||
"32968": "int",
|
|
||||||
"32969": "int",
|
|
||||||
"32970": "int",
|
|
||||||
"32971": "int",
|
|
||||||
"33000": "int | nil",
|
|
||||||
"33001": "int | nil",
|
|
||||||
"33170": "int",
|
|
||||||
"33901": "array[double, 2]",
|
|
||||||
"33902": "array[double, 2]",
|
|
||||||
"34016": "int",
|
|
||||||
"34024": "int",
|
|
||||||
"34045": "int | nil",
|
|
||||||
"34047": "nil",
|
|
||||||
"34068": "nil",
|
|
||||||
"34076": "int",
|
|
||||||
"34467": "nil",
|
|
||||||
"34816": "int",
|
|
||||||
"34817": "int",
|
|
||||||
"34818": "int",
|
|
||||||
"34819": "int",
|
|
||||||
"34852": "int | nil",
|
|
||||||
"34853": "int | nil",
|
|
||||||
"34854": "int | nil",
|
|
||||||
"34855": "int | nil",
|
|
||||||
"34856": "int | nil",
|
|
||||||
"34857": "int | nil",
|
|
||||||
"34858": "int | nil",
|
|
||||||
"34859": "int | nil",
|
|
||||||
"34860": "int | nil",
|
|
||||||
"34877": "int",
|
|
||||||
"34921": "int",
|
|
||||||
"34930": "int",
|
|
||||||
"34964": "nil",
|
|
||||||
"34965": "nil",
|
|
||||||
"35071": "int | nil",
|
|
||||||
"35076": "int | nil",
|
|
||||||
"35077": "int | nil",
|
|
||||||
"35371": "int | nil",
|
|
||||||
"35373": "int | nil",
|
|
||||||
"35374": "int | nil",
|
|
||||||
"35375": "int | nil",
|
|
||||||
"35376": "int | nil",
|
|
||||||
"35377": "int | nil",
|
|
||||||
"35379": "int | nil",
|
|
||||||
"35380": "int | nil",
|
|
||||||
"35657": "int | nil",
|
|
||||||
"35658": "int | nil",
|
|
||||||
"35659": "int | nil",
|
|
||||||
"35660": "int",
|
|
||||||
"35661": "int",
|
|
||||||
"35723": "int | nil",
|
|
||||||
"35724": "str",
|
|
||||||
"35725": "nil",
|
|
||||||
"35738": "int",
|
|
||||||
"35739": "int",
|
|
||||||
"35968": "int | nil",
|
|
||||||
"35977": "bool | nil",
|
|
||||||
"35978": "int | nil",
|
|
||||||
"35979": "int | nil",
|
|
||||||
"36003": "int",
|
|
||||||
"36004": "int",
|
|
||||||
"36005": "int",
|
|
||||||
"36006": "nil",
|
|
||||||
"36007": "nil",
|
|
||||||
"36063": "int | nil",
|
|
||||||
"36183": "int | nil",
|
|
||||||
"36203": "int | nil",
|
|
||||||
"36345": "int | nil",
|
|
||||||
"36347": "int",
|
|
||||||
"36348": "int",
|
|
||||||
"36349": "int",
|
|
||||||
"36387": "bool | nil",
|
|
||||||
"36388": "bool | nil",
|
|
||||||
"36392": "nil",
|
|
||||||
"36795": "nil",
|
|
||||||
"37137": "int | double | nil",
|
|
||||||
"37154": "int | nil",
|
|
||||||
"37157": "int | nil",
|
|
||||||
"37440": "bool",
|
|
||||||
"37441": "bool",
|
|
||||||
"37443": "int",
|
|
||||||
"37444": "nil",
|
|
||||||
"37445": "str",
|
|
||||||
"37446": "str",
|
|
||||||
"37447": "int | nil",
|
|
||||||
"38449": "nil"
|
|
||||||
},
|
|
||||||
|
|
||||||
"@WEBGL_SHADER_PRECISION_FORMATS": {
|
|
||||||
"/^\\d+,\\d+$/": {
|
|
||||||
"*rangeMin": "int[>=0]",
|
|
||||||
"*rangeMax": "int[>=0]",
|
|
||||||
"*precision": "int[>=0]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"@WEBGL_CONTEXT_ATTRIBUTES": {
|
|
||||||
"alpha": "bool",
|
|
||||||
"antialias": "bool",
|
|
||||||
"depth": "bool",
|
|
||||||
"failIfMajorPerformanceCaveat": "bool",
|
|
||||||
"powerPreference": "str[low, high, default]",
|
|
||||||
"premultipliedAlpha": "bool",
|
|
||||||
"preserveDrawingBuffer": "bool",
|
|
||||||
"stencil": "bool"
|
|
||||||
},
|
|
||||||
|
|
||||||
"canvas:aaOffset": "int",
|
|
||||||
"canvas:aaCapOffset": "bool",
|
|
||||||
|
|
||||||
"voices": "array[@VOICE_TYPE]",
|
|
||||||
"voices:blockIfNotDefined": "bool",
|
|
||||||
"voices:fakeCompletion": "bool",
|
|
||||||
"voices:fakeCompletion:charsPerSecond": "double[>0]",
|
|
||||||
|
|
||||||
"@VOICE_TYPE": {
|
|
||||||
"*isLocalService": "bool",
|
|
||||||
"*isDefault": "bool",
|
|
||||||
"*voiceURI": "str",
|
|
||||||
"*name": "str",
|
|
||||||
"*lang": "str"
|
|
||||||
},
|
|
||||||
|
|
||||||
"humanize": "bool",
|
|
||||||
"humanize:maxTime": "double[>=0]",
|
|
||||||
"humanize:minTime": "double[>=0]",
|
|
||||||
"showcursor": "bool",
|
|
||||||
|
|
||||||
"allowMainWorld": "bool",
|
|
||||||
"forceScopeAccess": "bool",
|
|
||||||
"enableRemoteSubframes": "bool",
|
|
||||||
"disableTheming": "bool",
|
|
||||||
"memorysaver": "bool",
|
|
||||||
"addons": "array[str]",
|
|
||||||
"certificatePaths": "array[str]",
|
|
||||||
"certificates": "array[str]",
|
|
||||||
"debug": "bool"
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue