-
-
Notifications
You must be signed in to change notification settings - Fork 22
fix(config): Add defaults to schemas and fix config validation issues #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
8197c80
fix(web): Resolve font display and config API error handling issues
72b3ef0
fix(web): Prevent fontCatalog redeclaration error on HTMX reload
e129989
fix(web): Wrap fonts script in IIFE to prevent all redeclaration errors
5e1f977
fix(web): Exempt config save API endpoints from CSRF protection
c2bb400
fix(web): Exempt system action endpoint from CSRF protection
814a69e
fix(web): Exempt all API v3 endpoints from CSRF protection
b508548
refactor(web): Remove CSRF protection for local-only application
2ccf219
fix(web): Fix font path double-prefixing in font catalog display
4475d2b
fix(web): Remove fontsTabInitialized guard to allow re-initialization…
63f9f4b
fix(api): Preserve empty strings for optional string fields in plugin…
c2f55f1
fix(config): Add defaults to schemas and fix None value handling
2c45acb
fix(config): Fix save button spinner by checking HTTP status code
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Submodule mqtt-notifications
updated
from 3bd955 to 5c23a8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Script to add default values to plugin config schemas where missing. | ||
|
|
||
| This ensures that configs never start with None values, improving user experience | ||
| and preventing validation errors. | ||
| """ | ||
|
|
||
| import json | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Any, Dict, List, Optional | ||
|
|
||
|
|
||
| def get_default_for_field(prop: Dict[str, Any]) -> Any: | ||
| """ | ||
| Determine a sensible default value for a field based on its type and constraints. | ||
|
|
||
| Args: | ||
| prop: Field property schema | ||
|
|
||
| Returns: | ||
| Default value or None if no default should be added | ||
| """ | ||
| prop_type = prop.get('type') | ||
|
|
||
| # Handle union types (array with multiple types) | ||
| if isinstance(prop_type, list): | ||
| # Use the first non-null type | ||
| prop_type = next((t for t in prop_type if t != 'null'), prop_type[0] if prop_type else 'string') | ||
|
|
||
| if prop_type == 'boolean': | ||
| return False | ||
|
|
||
| elif prop_type == 'number': | ||
| # For numbers, use minimum if available, or a sensible default | ||
| minimum = prop.get('minimum') | ||
| maximum = prop.get('maximum') | ||
|
|
||
| if minimum is not None: | ||
| return minimum | ||
| elif maximum is not None: | ||
| # Use a reasonable fraction of max (like 30% or minimum 1) | ||
| return max(1, int(maximum * 0.3)) | ||
| else: | ||
| # No constraints, use 0 | ||
| return 0 | ||
|
|
||
| elif prop_type == 'integer': | ||
| # Similar to number | ||
| minimum = prop.get('minimum') | ||
| maximum = prop.get('maximum') | ||
|
|
||
| if minimum is not None: | ||
| return minimum | ||
| elif maximum is not None: | ||
| return max(1, int(maximum * 0.3)) | ||
| else: | ||
| return 0 | ||
|
|
||
| elif prop_type == 'string': | ||
| # Only add default for strings if it makes sense | ||
| # Check if there's an enum - use first value | ||
| enum_values = prop.get('enum') | ||
| if enum_values: | ||
| return enum_values[0] | ||
|
|
||
| # For optional string fields, empty string might be okay, but be cautious | ||
| # We'll skip adding defaults for strings unless explicitly needed | ||
| return None | ||
|
|
||
| elif prop_type == 'array': | ||
| # Empty array as default | ||
| return [] | ||
|
|
||
| elif prop_type == 'object': | ||
| # Empty object - but we'll handle nested objects separately | ||
| return {} | ||
|
|
||
| return None | ||
|
|
||
|
|
||
| def should_add_default(prop: Dict[str, Any], field_path: str) -> bool: | ||
| """ | ||
| Determine if we should add a default value to this field. | ||
|
|
||
| Args: | ||
| prop: Field property schema | ||
| field_path: Dot-separated path to the field | ||
|
|
||
| Returns: | ||
| True if default should be added | ||
| """ | ||
| # Skip if already has a default | ||
| if 'default' in prop: | ||
| return False | ||
|
|
||
| # Skip secret fields (they should be user-provided) | ||
| if prop.get('x-secret', False): | ||
| return False | ||
|
|
||
| # Skip API keys and similar sensitive fields | ||
| field_name = field_path.split('.')[-1].lower() | ||
| sensitive_keywords = ['key', 'password', 'secret', 'token', 'auth', 'credential'] | ||
| if any(keyword in field_name for keyword in sensitive_keywords): | ||
| return False | ||
|
|
||
| prop_type = prop.get('type') | ||
| if isinstance(prop_type, list): | ||
| prop_type = next((t for t in prop_type if t != 'null'), prop_type[0] if prop_type else None) | ||
|
|
||
| # Only add defaults for certain types | ||
| if prop_type in ('boolean', 'number', 'integer', 'array'): | ||
| return True | ||
|
|
||
| # For strings, only if there's an enum | ||
| if prop_type == 'string' and 'enum' in prop: | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
|
|
||
| def add_defaults_recursive(schema: Dict[str, Any], path: str = "", modified: List[str] = None) -> bool: | ||
| """ | ||
| Recursively add default values to schema fields. | ||
|
|
||
| Args: | ||
| schema: Schema dictionary to modify | ||
| path: Current path in the schema (for logging) | ||
| modified: List to track which fields were modified | ||
|
|
||
| Returns: | ||
| True if any modifications were made | ||
| """ | ||
| if modified is None: | ||
| modified = [] | ||
|
|
||
| if not isinstance(schema, dict) or 'properties' not in schema: | ||
| return False | ||
|
|
||
| changes_made = False | ||
|
|
||
| for key, prop in schema['properties'].items(): | ||
| if not isinstance(prop, dict): | ||
| continue | ||
|
|
||
| current_path = f"{path}.{key}" if path else key | ||
|
|
||
| # Check nested objects | ||
| if prop.get('type') == 'object' and 'properties' in prop: | ||
| if add_defaults_recursive(prop, current_path, modified): | ||
| changes_made = True | ||
|
|
||
| # Add default if appropriate | ||
| if should_add_default(prop, current_path): | ||
| default_value = get_default_for_field(prop) | ||
| if default_value is not None: | ||
| prop['default'] = default_value | ||
| modified.append(current_path) | ||
| changes_made = True | ||
| print(f" Added default to {current_path}: {default_value} (type: {prop.get('type')})") | ||
|
|
||
| return changes_made | ||
|
|
||
|
|
||
| def process_schema_file(schema_path: Path) -> bool: | ||
| """ | ||
| Process a single schema file to add defaults. | ||
|
|
||
| Args: | ||
| schema_path: Path to the schema file | ||
|
|
||
| Returns: | ||
| True if file was modified | ||
| """ | ||
| print(f"\nProcessing: {schema_path}") | ||
|
|
||
| try: | ||
| with open(schema_path, 'r', encoding='utf-8') as f: | ||
| schema = json.load(f) | ||
| except Exception as e: | ||
| print(f" Error reading schema: {e}") | ||
| return False | ||
|
|
||
| modified_fields = [] | ||
| changes_made = add_defaults_recursive(schema, modified=modified_fields) | ||
|
|
||
| if changes_made: | ||
| # Write back with pretty formatting | ||
| with open(schema_path, 'w', encoding='utf-8') as f: | ||
| json.dump(schema, f, indent=2, ensure_ascii=False) | ||
| f.write('\n') # Add trailing newline | ||
|
|
||
| print(f" ✓ Modified {len(modified_fields)} fields") | ||
| return True | ||
| else: | ||
| print(f" ✓ No changes needed") | ||
| return False | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point.""" | ||
| project_root = Path(__file__).parent.parent | ||
| plugins_dir = project_root / 'plugins' | ||
|
|
||
| if not plugins_dir.exists(): | ||
| print(f"Error: Plugins directory not found: {plugins_dir}") | ||
| sys.exit(1) | ||
|
|
||
| # Find all config_schema.json files | ||
| schema_files = list(plugins_dir.rglob('config_schema.json')) | ||
|
|
||
| if not schema_files: | ||
| print("No config_schema.json files found") | ||
| sys.exit(0) | ||
|
|
||
| print(f"Found {len(schema_files)} schema files") | ||
|
|
||
| modified_count = 0 | ||
| for schema_file in sorted(schema_files): | ||
| if process_schema_file(schema_file): | ||
| modified_count += 1 | ||
|
|
||
| print(f"\n{'='*60}") | ||
| print(f"Summary: Modified {modified_count} out of {len(schema_files)} schema files") | ||
| print(f"{'='*60}") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.