Coverage for src/pullapprove/confirmation.py: 71%
75 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-12-01 20:41 -0600
« prev ^ index » next coverage.py v7.8.2, created at 2025-12-01 20:41 -0600
1"""
2Configuration models for review confirmation forms.
4Follows GitHub's form schema structure:
5https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema
6"""
8from __future__ import annotations
10import hashlib
11import json
12from enum import Enum
13from typing import Any, Literal
15from pydantic import (
16 BaseModel,
17 ConfigDict,
18 Field,
19 field_validator,
20)
23class ConfirmationFieldType(str, Enum):
24 CHECKBOXES = "checkboxes"
25 DROPDOWN = "dropdown"
26 INPUT = "input"
27 TEXTAREA = "textarea"
30class CheckboxOption(BaseModel):
31 """A single checkbox option within a checkboxes field."""
33 model_config = ConfigDict(extra="forbid")
35 label: str = Field(min_length=1)
36 required: bool = False
39class ConfirmationFieldBase(BaseModel):
40 """Base class for confirmation form fields."""
42 model_config = ConfigDict(extra="forbid")
44 type: ConfirmationFieldType
45 id: str = Field(default="")
46 label: str = ""
47 description: str = ""
50class CheckboxesField(ConfirmationFieldBase):
51 """Multiple attestation checkboxes."""
53 type: Literal["checkboxes"]
54 id: str = Field(min_length=1)
55 label: str = Field(min_length=1)
56 options: list[CheckboxOption] = Field(min_length=1)
59class DropdownField(ConfirmationFieldBase):
60 """Single or multi-select dropdown."""
62 type: Literal["dropdown"]
63 id: str = Field(min_length=1)
64 label: str = Field(min_length=1)
65 options: list[str] = Field(min_length=1)
66 required: bool = False
67 multiple: bool = False
70class InputField(ConfirmationFieldBase):
71 """Single-line text input."""
73 type: Literal["input"]
74 id: str = Field(min_length=1)
75 label: str = Field(min_length=1)
76 placeholder: str = ""
77 required: bool = False
80class TextareaField(ConfirmationFieldBase):
81 """Multi-line text input."""
83 type: Literal["textarea"]
84 id: str = Field(min_length=1)
85 label: str = Field(min_length=1)
86 placeholder: str = ""
87 required: bool = False
90# Union type for all field types
91ConfirmationField = CheckboxesField | DropdownField | InputField | TextareaField
94class ConfirmationFormModel(BaseModel):
95 """Configuration for review confirmation forms."""
97 model_config = ConfigDict(extra="forbid")
99 fields: list[ConfirmationField] = Field(min_length=1)
101 @field_validator("fields", mode="before")
102 @classmethod
103 def parse_fields(cls, fields: list[dict[str, Any]]) -> list[ConfirmationField]:
104 """Parse field dicts into appropriate field types based on 'type' key."""
105 parsed = []
106 for field_data in fields:
107 field_type = field_data.get("type")
108 match field_type:
109 case "checkboxes":
110 parsed.append(CheckboxesField(**field_data))
111 case "dropdown":
112 parsed.append(DropdownField(**field_data))
113 case "input":
114 parsed.append(InputField(**field_data))
115 case "textarea":
116 parsed.append(TextareaField(**field_data))
117 case _:
118 raise ValueError(f"Unknown confirmation field type: {field_type}")
119 return parsed
121 def compute_hash(self) -> str:
122 """Compute deterministic hash of form definition."""
123 # Serialize Pydantic models to dicts if needed
124 serialized: list[dict[str, Any]] = []
125 for f in self.fields:
126 if isinstance(f, dict):
127 serialized.append(f)
128 else:
129 # It's a Pydantic model
130 serialized.append(f.model_dump())
131 canonical = json.dumps(serialized, sort_keys=True)
132 return hashlib.sha256(canonical.encode()).hexdigest()