mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 04:52: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:
|
||||
python3 scripts/package.py linux \
|
||||
--includes \
|
||||
settings/omegacfg.jvv \
|
||||
settings/properties.json \
|
||||
bundle/fontconfigs \
|
||||
--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