File size: 4,821 Bytes
72de9a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""
Field validation helpers used by the graders.

Each validator returns True if the value matches the expected type,
False otherwise. These are intentionally simple and deterministic.
"""

import re
from typing import Any, Dict, List, Tuple


EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
PHONE_RE = re.compile(r"^\+?[1-9]\d{6,14}$")
URL_RE = re.compile(r"^https?://[^\s]+$")
ISO_DATETIME_RE = re.compile(
    r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$"
)


def validate_email(value: Any) -> bool:
    return isinstance(value, str) and bool(EMAIL_RE.match(value))


def validate_phone(value: Any) -> bool:
    if not isinstance(value, str):
        return False
    cleaned = value.replace(" ", "").replace("-", "")
    return bool(PHONE_RE.match(cleaned))


def validate_url(value: Any) -> bool:
    return isinstance(value, str) and bool(URL_RE.match(value))


def validate_datetime(value: Any) -> bool:
    return isinstance(value, str) and bool(ISO_DATETIME_RE.match(value))


def validate_enum(value: Any, allowed_values: List[str]) -> bool:
    return isinstance(value, str) and value in allowed_values


def validate_field_type(value: Any, expected_type: str) -> bool:
    """Check if a value matches the expected type string from the spec.

    Supported types: string, integer, float, boolean, email, datetime,
    url, phone, enum:val1,val2, object, array.
    """
    if value is None:
        return False

    if expected_type == "string":
        return isinstance(value, str)
    elif expected_type == "integer":
        return isinstance(value, int) and not isinstance(value, bool)
    elif expected_type == "float":
        return isinstance(value, (int, float)) and not isinstance(value, bool)
    elif expected_type == "boolean":
        return isinstance(value, bool)
    elif expected_type == "email":
        return validate_email(value)
    elif expected_type == "datetime":
        return validate_datetime(value)
    elif expected_type == "url":
        return validate_url(value)
    elif expected_type == "phone":
        return validate_phone(value)
    elif expected_type.startswith("enum:"):
        allowed = expected_type.split(":", 1)[1].split(",")
        return validate_enum(value, allowed)
    elif expected_type == "object":
        return isinstance(value, dict)
    elif expected_type == "array":
        return isinstance(value, list)
    else:
        # Unknown type, accept anything non-None
        return True


def validate_request_against_spec(
    request: Dict[str, Any],
    spec: Dict[str, Any],
) -> Tuple[float, str]:
    """Validate a request body against its spec.

    Returns (score, feedback_string) where score is 0.0 to 1.0
    based on how many checks pass.
    """
    checks = []
    total = 0
    passed = 0

    # Check required fields are present and non-null
    for field in spec["required_fields"]:
        total += 1
        if field in request and request[field] is not None:
            passed += 1
            checks.append(f"  {field}: PRESENT")
        else:
            checks.append(f"  {field}: MISSING")

    # Check field types for fields that are present
    for field, expected_type in spec["field_types"].items():
        if field not in request or request[field] is None:
            continue
        total += 1
        if validate_field_type(request[field], expected_type):
            passed += 1
            checks.append(f"  {field} type: VALID ({expected_type})")
        else:
            checks.append(f"  {field} type: INVALID (expected {expected_type})")

    # Check no extra unknown fields
    all_known = set(spec["required_fields"]) | set(spec.get("optional_fields", []))
    for field in request:
        if field not in all_known:
            total += 1
            checks.append(f"  {field}: UNKNOWN FIELD (not in spec)")

    score = passed / total if total > 0 else 0.0
    feedback = f"Validation: {passed}/{total} checks passed.\n" + "\n".join(checks)
    return round(score, 4), feedback


def validate_headers_against_spec(
    headers: Dict[str, str],
    spec: Dict[str, Any],
) -> Tuple[float, str]:
    """Validate request headers against the spec's required_headers.

    Returns (score, feedback_string).
    """
    required = spec.get("required_headers", {})
    if not required:
        return 1.0, "No required headers."

    total = len(required)
    passed = 0
    checks = []

    for header_name in required:
        if header_name in headers:
            passed += 1
            checks.append(f"  {header_name}: PRESENT")
        else:
            checks.append(f"  {header_name}: MISSING")

    score = passed / total if total > 0 else 1.0
    feedback = f"Headers: {passed}/{total} present.\n" + "\n".join(checks)
    return round(score, 4), feedback