mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 04:52:03 -08:00
jsonvv: Add grouped keys syntax 0.2.0
This commit is contained in:
parent
20f4aa00e9
commit
cad90e30aa
7 changed files with 92 additions and 22 deletions
|
|
@ -76,7 +76,7 @@ validator = {
|
|||
"name,type": "str - str[Traveling]", # Non-traveling types
|
||||
# If hour(s) is specified, require days have >0 hours
|
||||
"/hours?/": {
|
||||
"*/.*day/": "int[>0]"
|
||||
"*/day$/": "int[>0]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +110,7 @@ else:
|
|||
- [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)
|
||||
|
|
@ -160,7 +161,7 @@ To specify a list of keys, use a comma-separated string.
|
|||
"/k[ey]{2}1/,key2": "type"
|
||||
```
|
||||
|
||||
To escape a comma, use `_`.
|
||||
To escape a comma, use `!`.
|
||||
|
||||
### Required fields (`*`)
|
||||
|
||||
|
|
@ -173,6 +174,20 @@ Fields marked with `*` are required. The validation will fail without them.
|
|||
"*/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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Supported Types
|
||||
|
|
@ -621,6 +636,7 @@ graph TD
|
|||
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>"]
|
||||
|
||||
|
|
@ -634,7 +650,7 @@ graph TD
|
|||
class JvvException jvv;
|
||||
class JvvRuntimeException,JvvSyntaxError runtime;
|
||||
class PropertySyntaxError syntax;
|
||||
class UnknownProperty,InvalidPropertyType,MissingRequiredKey error;
|
||||
class UnknownProperty,InvalidPropertyType,MissingRequiredKey,MissingGroupKey error;
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -704,3 +720,8 @@ graph TD
|
|||
- **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.
|
||||
|
|
@ -7,7 +7,7 @@ from .exceptions import (
|
|||
PropertySyntaxError,
|
||||
UnknownProperty,
|
||||
)
|
||||
from .validator import JsonValidator, validate_config
|
||||
from .validator import JsonValidator
|
||||
|
||||
__all__ = [
|
||||
'JvvRuntimeException',
|
||||
|
|
@ -18,5 +18,4 @@ __all__ = [
|
|||
'InvalidPropertyType',
|
||||
'UnknownProperty',
|
||||
'MissingRequiredKey',
|
||||
'validate_config',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -25,5 +25,9 @@ class MissingRequiredKey(InvalidPropertyType):
|
|||
pass
|
||||
|
||||
|
||||
class MissingGroupKey(MissingRequiredKey):
|
||||
pass
|
||||
|
||||
|
||||
class PropertySyntaxError(JvvSyntaxError):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class Parser:
|
|||
|
||||
# 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.type_str[self.pos].isalnum() or self.type_str[self.pos] in '!@'
|
||||
):
|
||||
self.pos += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ class StringValidator:
|
|||
i = 0
|
||||
|
||||
while i < len(p):
|
||||
if p[i] == '/' and (i == 0 or p[i - 1] != '_'):
|
||||
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] == '_':
|
||||
if i > 0 and p[i - 1] == '!':
|
||||
current.append(',')
|
||||
else:
|
||||
# End of pattern
|
||||
|
|
@ -36,23 +36,22 @@ class StringValidator:
|
|||
return result
|
||||
|
||||
def _is_regex_pattern(self, p: str) -> bool:
|
||||
is_regex = p.startswith('/') and p.endswith('/') and not p.endswith('_/')
|
||||
is_regex = p.startswith('/') and p.endswith('/') and not p.endswith('!/')
|
||||
return is_regex
|
||||
|
||||
def _clean_literal_pattern(self, p: str) -> str:
|
||||
cleaned = re.sub(r'_[^_]', lambda m: m.group(0)[-1], p)
|
||||
return cleaned
|
||||
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:
|
||||
cleaned = self._clean_literal_pattern(p)
|
||||
match = value == cleaned
|
||||
match = value == p
|
||||
if match:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from .exceptions import MissingRequiredKey, PropertySyntaxError, UnknownProperty
|
||||
from .exceptions import (
|
||||
MissingGroupKey,
|
||||
MissingRequiredKey,
|
||||
PropertySyntaxError,
|
||||
UnknownProperty,
|
||||
)
|
||||
from .parser import parse_type_def
|
||||
from .strings import string_validator
|
||||
from .types import Type
|
||||
|
|
@ -12,10 +17,15 @@ class JsonValidator:
|
|||
# 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 = ""):
|
||||
|
|
@ -37,6 +47,13 @@ class JsonValidator:
|
|||
f"Invalid key '{current_path}': '*' must be followed by a property name"
|
||||
)
|
||||
|
||||
# Register group dependencies
|
||||
if (idx := key.rfind('$')) != -1:
|
||||
base_key, group = key[:idx], key[idx + 1 :]
|
||||
if group not in self.groups:
|
||||
self.groups[group] = []
|
||||
self.groups[group].append(base_key)
|
||||
|
||||
if isinstance(value, dict):
|
||||
# Recursively validate and parse nested dictionaries
|
||||
self.parse_types(value, current_path)
|
||||
|
|
@ -53,6 +70,32 @@ class JsonValidator:
|
|||
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],
|
||||
|
|
@ -75,8 +118,11 @@ def validate_config(
|
|||
type_def = None
|
||||
current_path = f"{path}.{key}" if path else key
|
||||
|
||||
if key in property_types:
|
||||
type_def = property_types[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):
|
||||
|
|
@ -85,15 +131,16 @@ def validate_config(
|
|||
)
|
||||
continue
|
||||
|
||||
elif '*' + key in property_types:
|
||||
type_def = property_types['*' + key]
|
||||
required_props[key] = True
|
||||
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
|
||||
if string_validator(key, pattern):
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||
|
||||
[tool.poetry]
|
||||
name = "jsonvv"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "JSON value validator"
|
||||
authors = ["daijro <daijro.dev@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue