v0-json-schema
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Base adapter interface for rule storage."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
|
||||
from ...models.rule_models import AnyRule, RuleQuery, BaseRule
|
||||
|
||||
|
||||
class BaseRuleStorageAdapter(ABC):
|
||||
"""Base class for rule storage adapters."""
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self) -> None:
|
||||
"""初始化适配器(例如,连接到数据库,验证文件路径等)。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
按ID加载单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,则使用配置的默认版本策略。
|
||||
|
||||
Returns:
|
||||
找到的规则对象,或者如果未找到则返回None。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件返回匹配的规则列表。
|
||||
|
||||
Args:
|
||||
query: 规则查询条件。
|
||||
|
||||
Returns:
|
||||
匹配规则的列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存单个规则。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则对象。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,通常会删除所有版本。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_all_rule_ids(self) -> List[str]:
|
||||
"""
|
||||
列出存储中的所有规则ID。
|
||||
|
||||
Returns:
|
||||
规则ID的列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_rule_versions(self, rule_id: str) -> List[str]:
|
||||
"""
|
||||
获取特定规则ID的所有可用版本。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
|
||||
Returns:
|
||||
版本标识符的列表。
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,324 @@
|
||||
"""File system adapter for rule storage using JSON files."""
|
||||
import os
|
||||
import json
|
||||
import glob
|
||||
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .base_adapter import BaseRuleStorageAdapter
|
||||
from ...models.rule_models import (
|
||||
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType
|
||||
)
|
||||
from .rule_adapter_utils import parse_rule_data
|
||||
|
||||
class FilesystemAdapter(BaseRuleStorageAdapter):
|
||||
"""
|
||||
基于文件系统的规则存储适配器,使用JSON文件保存规则。
|
||||
|
||||
文件结构约定:
|
||||
规则文件将按以下方式组织:
|
||||
rules/
|
||||
json_schemas/
|
||||
rule_id1/
|
||||
1.0.0.json
|
||||
1.1.0.json
|
||||
rule_id2/
|
||||
1.0.0.json
|
||||
business_logic/
|
||||
rule_id3/
|
||||
1.0.0.json
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.json"):
|
||||
"""
|
||||
初始化适配器。
|
||||
|
||||
Args:
|
||||
base_path: 存储规则的基本目录路径。
|
||||
file_pattern: 匹配规则文件的glob模式。
|
||||
"""
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
self.file_pattern = file_pattern
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""确保基本目录存在,并验证其可访问性。"""
|
||||
if not os.path.exists(self.base_path):
|
||||
try:
|
||||
os.makedirs(self.base_path)
|
||||
self.logger.info(f"Created rules directory at {self.base_path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to create rules directory at {self.base_path}: {e}")
|
||||
raise ValueError(f"Rules directory {self.base_path} does not exist and could not be created")
|
||||
|
||||
if not os.access(self.base_path, os.R_OK | os.W_OK):
|
||||
self.logger.error(f"Rules directory {self.base_path} is not readable and writable")
|
||||
raise ValueError(f"Rules directory {self.base_path} is not readable and writable")
|
||||
|
||||
# 确保每个规则类别的子目录存在
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
try:
|
||||
os.makedirs(category_dir)
|
||||
self.logger.debug(f"Created category directory at {category_dir}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to create category directory at {category_dir}: {e}")
|
||||
|
||||
def _get_category_dir(self, category: RuleCategory) -> str:
|
||||
"""获取给定规则类别的目录路径。"""
|
||||
return os.path.join(self.base_path, category.value.lower())
|
||||
|
||||
def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""
|
||||
获取给定规则ID的目录路径。
|
||||
如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。
|
||||
"""
|
||||
if category:
|
||||
return os.path.join(self._get_category_dir(category), rule_id)
|
||||
|
||||
# 如果未指定类别,检查所有类别目录
|
||||
for cat in RuleCategory:
|
||||
rule_dir = os.path.join(self._get_category_dir(cat), rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
return rule_dir
|
||||
|
||||
# 如果未找到任何匹配项,默认返回通用类别目录
|
||||
return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id)
|
||||
|
||||
def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""获取给定规则ID和版本的文件路径。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
return os.path.join(rule_dir, f"{version}.json")
|
||||
|
||||
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
|
||||
"""从文件加载规则。"""
|
||||
if not os.path.exists(file_path):
|
||||
self.logger.debug(f"Rule file {file_path} does not exist")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
raw_data = json.load(f)
|
||||
|
||||
# 使用工具函数解析规则数据
|
||||
return parse_rule_data(raw_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Failed to parse JSON from {file_path}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading rule from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool:
|
||||
"""将规则保存到文件。"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
# 序列化规则为JSON并写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(rule.model_dump(), f, indent=2, ensure_ascii=False)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to save rule to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]:
|
||||
"""获取给定规则ID的最新版本。"""
|
||||
versions = self.get_rule_versions(rule_id, category)
|
||||
if not versions:
|
||||
return None
|
||||
|
||||
# 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本
|
||||
# 如果需要更复杂的版本比较,可以使用packaging.version
|
||||
versions.sort()
|
||||
return versions[-1]
|
||||
|
||||
def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]:
|
||||
"""获取给定规则ID的所有版本。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
if not os.path.exists(rule_dir):
|
||||
return []
|
||||
|
||||
# 查找目录中所有匹配的JSON文件,并提取版本号(文件名)
|
||||
pattern = os.path.join(rule_dir, self.file_pattern)
|
||||
version_files = glob.glob(pattern)
|
||||
|
||||
versions = []
|
||||
for vf in version_files:
|
||||
version = os.path.splitext(os.path.basename(vf))[0] # 移除.json扩展名
|
||||
versions.append(version)
|
||||
|
||||
return versions
|
||||
|
||||
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
按ID加载单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,则加载最新版本。
|
||||
|
||||
Returns:
|
||||
找到的规则对象,或者如果未找到则返回None。
|
||||
"""
|
||||
if not version:
|
||||
version = self._get_latest_version(rule_id)
|
||||
if not version:
|
||||
self.logger.debug(f"No versions found for rule ID {rule_id}")
|
||||
return None
|
||||
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
return self._get_rule_from_file(file_path)
|
||||
|
||||
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件查询规则。
|
||||
|
||||
Args:
|
||||
query: 包含筛选条件的查询对象。
|
||||
|
||||
Returns:
|
||||
匹配查询条件的规则列表。
|
||||
"""
|
||||
results = []
|
||||
|
||||
# 如果指定了规则ID,直接加载该规则
|
||||
if query.rule_id:
|
||||
rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
return results
|
||||
|
||||
# 否则,根据查询条件扫描规则文件
|
||||
categories_to_search = [query.category] if query.category else list(RuleCategory)
|
||||
|
||||
for category in categories_to_search:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
for rule_id in rule_dirs:
|
||||
# 对于每个规则ID,加载指定版本或最新版本
|
||||
if query.version and query.version != "latest":
|
||||
file_path = self._get_rule_file_path(rule_id, query.version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
else:
|
||||
latest_version = self._get_latest_version(rule_id, category)
|
||||
if latest_version:
|
||||
file_path = self._get_rule_file_path(rule_id, latest_version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
|
||||
return results
|
||||
|
||||
def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool:
|
||||
"""检查规则是否匹配查询条件。"""
|
||||
# 检查是否启用
|
||||
if query.is_enabled is not None and rule.is_enabled != query.is_enabled:
|
||||
return False
|
||||
|
||||
# 检查目标类型
|
||||
if query.target_type and rule.target_type != query.target_type:
|
||||
return False
|
||||
|
||||
# 检查目标标识符
|
||||
if query.target_identifier and rule.target_identifier != query.target_identifier:
|
||||
return False
|
||||
|
||||
# 检查标签
|
||||
if query.tags:
|
||||
if not rule.tags:
|
||||
return False
|
||||
# 检查是否所有查询标签都在规则标签中
|
||||
if not all(tag in rule.tags for tag in query.tags):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存规则到文件系统。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则对象。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
file_path = self._get_rule_file_path(rule.id, rule.version, rule.category)
|
||||
return self._save_rule_to_file(rule, file_path)
|
||||
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 如果提供,仅删除该版本;否则删除所有版本。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
try:
|
||||
if version:
|
||||
# 删除特定版本
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
self.logger.info(f"Deleted rule file: {file_path}")
|
||||
else:
|
||||
self.logger.warning(f"Rule file not found for deletion: {file_path}")
|
||||
return False
|
||||
else:
|
||||
# 删除所有版本(整个规则目录)
|
||||
rule_dir = self._get_rule_dir(rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
# 递归删除目录及其内容
|
||||
for root, dirs, files in os.walk(rule_dir, topdown=False):
|
||||
for file in files:
|
||||
os.remove(os.path.join(root, file))
|
||||
for dir in dirs:
|
||||
os.rmdir(os.path.join(root, dir))
|
||||
os.rmdir(rule_dir)
|
||||
self.logger.info(f"Deleted rule directory: {rule_dir}")
|
||||
else:
|
||||
self.logger.warning(f"Rule directory not found for deletion: {rule_dir}")
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}")
|
||||
return False
|
||||
|
||||
def list_all_rule_ids(self) -> List[str]:
|
||||
"""列出所有规则ID。"""
|
||||
all_rule_ids = set()
|
||||
|
||||
# 扫描所有类别目录
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
all_rule_ids.update(rule_dirs)
|
||||
|
||||
return list(all_rule_ids)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""规则适配器工具函数"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Type
|
||||
|
||||
from ...models.rule_models import (
|
||||
AnyRule, BaseRule, RuleCategory,
|
||||
JSONSchemaDefinition, APILintingRuleset, BusinessAssertionTemplate, DataQualityRule,
|
||||
PythonCodeRule, PerformanceRule, SecurityRule, RESTfulDesignRule, ErrorHandlingRule
|
||||
)
|
||||
|
||||
# 规则类别到Pydantic模型类的映射
|
||||
RULE_CLASSES = {
|
||||
RuleCategory.JSON_SCHEMA: JSONSchemaDefinition,
|
||||
RuleCategory.API_LINTING: APILintingRuleset,
|
||||
RuleCategory.BUSINESS_LOGIC: BusinessAssertionTemplate,
|
||||
RuleCategory.DATA_QUALITY: DataQualityRule,
|
||||
RuleCategory.PYTHON_CODE: PythonCodeRule,
|
||||
RuleCategory.PERFORMANCE: PerformanceRule,
|
||||
RuleCategory.SECURITY: SecurityRule,
|
||||
RuleCategory.API_DESIGN: RESTfulDesignRule,
|
||||
RuleCategory.ERROR_HANDLING: ErrorHandlingRule,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_rule_class_by_category(category: RuleCategory) -> Type[BaseRule]:
|
||||
"""根据规则类别获取对应的规则类"""
|
||||
return RULE_CLASSES.get(category, BaseRule)
|
||||
|
||||
def parse_rule_data(raw_data: Dict[str, Any]) -> Optional[AnyRule]:
|
||||
"""
|
||||
解析规则数据,返回对应类型的规则对象
|
||||
|
||||
Args:
|
||||
raw_data: 从文件加载的原始规则数据
|
||||
|
||||
Returns:
|
||||
规则对象,如果无法解析则返回None
|
||||
"""
|
||||
try:
|
||||
# 确保数据包含类别信息
|
||||
if 'category' not in raw_data:
|
||||
logger.warning("Rule data missing 'category' field")
|
||||
return None
|
||||
|
||||
# 尝试将字符串类别转换为枚举值
|
||||
try:
|
||||
category = RuleCategory(raw_data['category'])
|
||||
except ValueError:
|
||||
logger.warning(f"Rule data has invalid category: {raw_data['category']}")
|
||||
return None
|
||||
|
||||
# 获取对应的Pydantic模型类
|
||||
rule_class = get_rule_class_by_category(category)
|
||||
|
||||
# 使用Pydantic模型解析数据
|
||||
return rule_class(**raw_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing rule data: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user