diff --git a/src/templatebot/storage/slack/blockkit.py b/src/templatebot/storage/slack/blockkit.py index acfe038..e67a852 100644 --- a/src/templatebot/storage/slack/blockkit.py +++ b/src/templatebot/storage/slack/blockkit.py @@ -2,6 +2,7 @@ from __future__ import annotations +from abc import ABC from typing import Annotated, Literal, Self from pydantic import BaseModel, Field, model_validator @@ -19,7 +20,8 @@ "SlackSectionBlock", "SlackSectionBlockAccessoryTypes", "SlackStaticSelectElement", - "SlackTextObjectTypes", + "SlackTextObjectBase", + "SlackTextObjectType", ] block_id_field = Field( @@ -44,7 +46,7 @@ class SlackSectionBlock(BaseModel): block_id: Annotated[str | None, block_id_field] = None - text: SlackTextObjectTypes | None = Field( + text: SlackTextObjectType | None = Field( None, description=( "The text to display in the block. Not required if `fields` is " @@ -53,7 +55,7 @@ class SlackSectionBlock(BaseModel): ) # Fields can take other types of elements. - fields: list[SlackTextObjectTypes] | None = Field( + fields: list[SlackTextObjectType] | None = Field( None, description=( "An array of text objects. Each element of the array is a " @@ -116,7 +118,7 @@ class SlackContextBlock(BaseModel): block_id: Annotated[str | None, block_id_field] = None # image elements can also be supported when available - elements: list[SlackTextObjectTypes] = Field( + elements: list[SlackTextObjectType] = Field( ..., description=( "An array of text objects. Each element of the array is a " @@ -150,6 +152,7 @@ class SlackInputBlock(BaseModel): "A label that appears above an input element. " "Maximum length of 2000 characters." ), + max_length=2000, ) element: SlackStaticSelectElement | SlackPlainTextInputElement = Field( @@ -171,6 +174,7 @@ class SlackInputBlock(BaseModel): "apppears below an input element in a lighter font. " "Maximum length of 2000 characters." ), + max_length=2000, ) optional: bool = Field( @@ -181,17 +185,22 @@ class SlackInputBlock(BaseModel): ), ) - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 2000 characters.""" - if len(self.label.text) > 2000: - raise ValueError("The length of the label text must be <= 2000.") - if self.hint and len(self.hint.text) > 2000: - raise ValueError("The length of the hint text must be <= 2000.") - return self + +class SlackTextObjectBase(BaseModel, ABC): + """A base class for Slack Block Kit text objects.""" + + type: Literal["plain_text", "mrkdwn"] = Field( + ..., description="The type of object." + ) + + text: str = Field(..., description="The text to display.") + + def __len__(self) -> int: + """Return the length of the text.""" + return len(self.text) -class SlackPlainTextObject(BaseModel): +class SlackPlainTextObject(SlackTextObjectBase): """A plain_text composition object. https://api.slack.com/reference/block-kit/composition-objects#text @@ -201,8 +210,6 @@ class SlackPlainTextObject(BaseModel): "plain_text", description="The type of object." ) - text: str = Field(..., description="The text to display.") - emoji: bool = Field( True, description=( @@ -212,7 +219,7 @@ class SlackPlainTextObject(BaseModel): ) -class SlackMrkdwnTextObject(BaseModel): +class SlackMrkdwnTextObject(SlackTextObjectBase): """A mrkdwn text composition object. https://api.slack.com/reference/block-kit/composition-objects#text @@ -222,8 +229,6 @@ class SlackMrkdwnTextObject(BaseModel): "mrkdwn", description="The type of object." ) - text: str = Field(..., description="The text to display.") - verbatim: bool = Field( False, description=( @@ -234,6 +239,10 @@ class SlackMrkdwnTextObject(BaseModel): ) +SlackTextObjectType = SlackPlainTextObject | SlackMrkdwnTextObject +"""A type alias for Slack Block Kit text objects.""" + + class SlackOptionObject(BaseModel): """An option object for Slack Block Kit elements. @@ -249,6 +258,7 @@ class SlackOptionObject(BaseModel): "A plain text object that defines the text shown in the option. " "Maximum length of 75 characters." ), + max_length=75, ) value: str = Field( @@ -267,6 +277,7 @@ class SlackOptionObject(BaseModel): "A plain text object that defines a line of descriptive text " "shown below the text. Maximum length of 75 characters." ), + max_length=75, ) url: str | None = Field( @@ -280,15 +291,6 @@ class SlackOptionObject(BaseModel): max_length=3000, ) - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 75 characters.""" - if len(self.text.text) > 75: - raise ValueError("The length of the text must be <= 75.") - if self.description and len(self.description.text) > 75: - raise ValueError("The length of the description must be <= 75.") - return self - class SlackOptionGroupObject(BaseModel): """An option group object for Slack Block Kit elements. @@ -304,6 +306,7 @@ class SlackOptionGroupObject(BaseModel): "A plain text object that defines the label shown above this " "group of options. Maximum length of 75 characters." ), + max_length=75, ) options: list[SlackOptionObject] = Field( @@ -315,13 +318,6 @@ class SlackOptionGroupObject(BaseModel): max_length=100, ) - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 75 characters.""" - if len(self.label.text) > 75: - raise ValueError("The length of the label text must be <= 75.") - return self - class SlackConfirmationDialogObject(BaseModel): """A confirmation dialog object for Slack Block Kit elements. @@ -335,6 +331,7 @@ class SlackConfirmationDialogObject(BaseModel): "A plain text object that defines the dialog's title. Maximum " "length of 100 characters." ), + max_length=100, ) text: SlackPlainTextObject = Field( @@ -343,6 +340,7 @@ class SlackConfirmationDialogObject(BaseModel): "A text object that defines the explanatory text that appears in " "the confirm dialog. Maximum length of 300 characters." ), + max_length=300, ) confirm: SlackPlainTextObject = Field( @@ -351,6 +349,7 @@ class SlackConfirmationDialogObject(BaseModel): "A plain text object that defines the text of the button that " "confirms the action. Maximum length of 30 characters." ), + max_length=30, ) deny: SlackPlainTextObject = Field( @@ -359,6 +358,7 @@ class SlackConfirmationDialogObject(BaseModel): "A plain text object that defines the text of the button that " "denies the action. Maximum length of 30 characters." ), + max_length=30, ) style: Literal["primary", "danger"] = Field( @@ -369,19 +369,6 @@ class SlackConfirmationDialogObject(BaseModel): ), ) - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 300 characters.""" - if len(self.title.text) > 100: - raise ValueError("The length of the title text must be <= 100.") - if len(self.text.text) > 300: - raise ValueError("The length of the text must be <= 300.") - if len(self.confirm.text) > 300: - raise ValueError("The length of the confirm text must be <= 30.") - if len(self.deny.text) > 300: - raise ValueError("The length of the deny text must be <= 30.") - return self - class SlackStaticSelectElement(BaseModel): """A static select element for Slack Block Kit. @@ -403,6 +390,7 @@ class SlackStaticSelectElement(BaseModel): "A plain text object that defines the placeholder text shown on " "the static select element. Maximum length of 150 characters." ), + max_length=150, ) options: list[SlackOptionObject] | None = Field( @@ -468,15 +456,6 @@ def validate_options_or_option_groups(self) -> Self: ) return self - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 150 characters.""" - if len(self.placeholder.text) > 150: - raise ValueError( - "The length of the placeholder text must be <= 150." - ) - return self - class SlackPlainTextInputElement(BaseModel): """A plain text input element for Slack Block Kit. @@ -514,6 +493,7 @@ class SlackPlainTextInputElement(BaseModel): "A plain text object that defines the placeholder text shown in " "the plain-text input. Maximum length of 150 characters." ), + max_length=150, ) multiline: bool = Field( @@ -550,15 +530,6 @@ class SlackPlainTextInputElement(BaseModel): # dispatch_action_config is not implemented yet. - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text length is not more than 150 characters.""" - if self.placeholder and len(self.placeholder.text) > 150: - raise ValueError( - "The length of the placeholder text must be <= 150." - ) - return self - @model_validator(mode="after") def validate_min_max_length(self) -> Self: """Ensure that the min_length is less than or equal to max_length.""" @@ -576,8 +547,5 @@ def validate_min_max_length(self) -> Self: SlackBlock = SlackSectionBlock | SlackContextBlock | SlackInputBlock """A generic type alias for Slack Block Kit blocks.""" -SlackTextObjectTypes = SlackPlainTextObject | SlackMrkdwnTextObject -"""A type alias for Slack Block Kit text objects.""" - SlackSectionBlockAccessoryTypes = SlackStaticSelectElement """A type alias for Slack Block Kit section block accessory types.""" diff --git a/src/templatebot/storage/slack/views.py b/src/templatebot/storage/slack/views.py index 7c3c31a..1292960 100644 --- a/src/templatebot/storage/slack/views.py +++ b/src/templatebot/storage/slack/views.py @@ -2,9 +2,9 @@ from __future__ import annotations -from typing import Literal, Self +from typing import Literal -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field from .blockkit import SlackBlock, SlackPlainTextObject @@ -19,6 +19,7 @@ class SlackModalView(BaseModel): title: SlackPlainTextObject = Field( ..., description="The title of the view. Maximum length is 24 characters.", + max_length=24, ) blocks: list[SlackBlock] = Field( @@ -30,6 +31,7 @@ class SlackModalView(BaseModel): description=( "The text for the close button. Maximum length is 24 characters." ), + max_length=24, ) submit: SlackPlainTextObject | None = Field( @@ -37,6 +39,7 @@ class SlackModalView(BaseModel): description=( "The text for the submit button. Maximum length is 24 characters." ), + max_length=24, ) private_metadata: str | None = Field( @@ -72,12 +75,3 @@ class SlackModalView(BaseModel): external_id: str | None = Field( None, description="A unique identifier for the view." ) - - @model_validator(mode="after") - def validate_text_length(self) -> Self: - """Ensure that the text fields are within the maximum length.""" - for field in ("title", "close", "submit"): - value = getattr(self, field) - if value and len(value.text) > 24: - raise ValueError(f"{field} must be 24 characters or fewer.") - return self diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/storage/slack/__init__.py b/tests/storage/slack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/storage/slack/blockkit_test.py b/tests/storage/slack/blockkit_test.py new file mode 100644 index 0000000..2fe652b --- /dev/null +++ b/tests/storage/slack/blockkit_test.py @@ -0,0 +1,40 @@ +"""Tests for Block Kit models.""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel, Field, ValidationError + +from templatebot.storage.slack import blockkit + + +def test_plain_text_object_length() -> None: + """Test that the length of a plain text object is correct.""" + + class Model(BaseModel): + text: blockkit.SlackPlainTextObject = Field(..., max_length=5) + + data = Model.model_validate( + {"text": {"type": "plain_text", "text": "Hello"}} + ) + assert data.text.text == "Hello" + assert data.text.type == "plain_text" + + with pytest.raises(ValidationError): + Model.model_validate( + {"text": {"type": "plain_text", "text": "Hello!"}} + ) + + +def test_mrkdwn_text_object_length() -> None: + """Test that the length of a mrkdwn text object is correct.""" + + class Model(BaseModel): + text: blockkit.SlackMrkdwnTextObject = Field(..., max_length=5) + + data = Model.model_validate({"text": {"type": "mrkdwn", "text": "Hello"}}) + assert data.text.text == "Hello" + assert data.text.type == "mrkdwn" + + with pytest.raises(ValidationError): + Model.model_validate({"text": {"type": "mrkdwn", "text": "Hello!"}})