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

1""" 

2Configuration models for review confirmation forms. 

3 

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""" 

7 

8from __future__ import annotations 

9 

10import hashlib 

11import json 

12from enum import Enum 

13from typing import Any, Literal 

14 

15from pydantic import ( 

16 BaseModel, 

17 ConfigDict, 

18 Field, 

19 field_validator, 

20) 

21 

22 

23class ConfirmationFieldType(str, Enum): 

24 CHECKBOXES = "checkboxes" 

25 DROPDOWN = "dropdown" 

26 INPUT = "input" 

27 TEXTAREA = "textarea" 

28 

29 

30class CheckboxOption(BaseModel): 

31 """A single checkbox option within a checkboxes field.""" 

32 

33 model_config = ConfigDict(extra="forbid") 

34 

35 label: str = Field(min_length=1) 

36 required: bool = False 

37 

38 

39class ConfirmationFieldBase(BaseModel): 

40 """Base class for confirmation form fields.""" 

41 

42 model_config = ConfigDict(extra="forbid") 

43 

44 type: ConfirmationFieldType 

45 id: str = Field(default="") 

46 label: str = "" 

47 description: str = "" 

48 

49 

50class CheckboxesField(ConfirmationFieldBase): 

51 """Multiple attestation checkboxes.""" 

52 

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) 

57 

58 

59class DropdownField(ConfirmationFieldBase): 

60 """Single or multi-select dropdown.""" 

61 

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 

68 

69 

70class InputField(ConfirmationFieldBase): 

71 """Single-line text input.""" 

72 

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 

78 

79 

80class TextareaField(ConfirmationFieldBase): 

81 """Multi-line text input.""" 

82 

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 

88 

89 

90# Union type for all field types 

91ConfirmationField = CheckboxesField | DropdownField | InputField | TextareaField 

92 

93 

94class ConfirmationFormModel(BaseModel): 

95 """Configuration for review confirmation forms.""" 

96 

97 model_config = ConfigDict(extra="forbid") 

98 

99 fields: list[ConfirmationField] = Field(min_length=1) 

100 

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 

120 

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()