Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add validation for text alignment field in FieldContent struct #162

Merged
merged 3 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions lib/structs/field_content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ defmodule ExPass.Structs.FieldContent do
* "PKNumberStyleScientific"
* "PKNumberStyleSpellOut"

- `text_alignment`: The alignment of the text in the field.
Supported values are:
* "PKTextAlignmentLeft"
* "PKTextAlignmentCenter"
* "PKTextAlignmentRight"
* "PKTextAlignmentNatural"

- `value`: The value to use for the field. This can be a localizable string, ISO 8601 date, or number.
This field is required. A date or time value must include a time zone.
"""
Expand Down Expand Up @@ -129,6 +136,17 @@ defmodule ExPass.Structs.FieldContent do
"""
@type number_style() :: String.t()

@typedoc """
The alignment of the text in the field.

Optional. Valid values are:
- "PKTextAlignmentLeft"
- "PKTextAlignmentCenter"
- "PKTextAlignmentRight"
- "PKTextAlignmentNatural"
"""
@type text_alignment() :: String.t()

@typedoc """
The value to use for the field.

Expand All @@ -152,6 +170,7 @@ defmodule ExPass.Structs.FieldContent do
field :key, String.t(), enforce: true
field :label, String.t(), default: nil
field :number_style, number_style(), default: nil
field :text_alignment, text_alignment(), default: nil
field :value, value(), enforce: true
end

Expand All @@ -170,6 +189,7 @@ defmodule ExPass.Structs.FieldContent do
• key
• label
• number_style
• text_alignment
• value

## Parameters
Expand All @@ -187,22 +207,22 @@ defmodule ExPass.Structs.FieldContent do
## Examples

iex> FieldContent.new(%{key: "field1", value: "Hello, World!"})
%FieldContent{key: "field1", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil, ignores_time_zone: nil, is_relative: nil, label: nil, number_style: nil, value: "Hello, World!"}
%FieldContent{key: "field1", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil, ignores_time_zone: nil, is_relative: nil, label: nil, number_style: nil, text_alignment: nil, value: "Hello, World!"}

iex> FieldContent.new(%{key: "field2", value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, number_style: "PKNumberStyleDecimal"})
%FieldContent{key: "field2", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, label: nil, number_style: "PKNumberStyleDecimal", value: 42}
iex> FieldContent.new(%{key: "field2", value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, number_style: "PKNumberStyleDecimal", text_alignment: "PKTextAlignmentCenter"})
%FieldContent{key: "field2", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, label: nil, number_style: "PKNumberStyleDecimal", text_alignment: "PKTextAlignmentCenter", value: 42}

iex> datetime = ~U[2023-04-15 14:30:00Z]
iex> field_content = FieldContent.new(%{key: "field3", value: datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true})
iex> %FieldContent{key: "field3", value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true} = field_content
iex> field_content = FieldContent.new(%{key: "field3", value: datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true, text_alignment: "PKTextAlignmentRight"})
iex> %FieldContent{key: "field3", value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true, text_alignment: "PKTextAlignmentRight"} = field_content
iex> field_content.change_message
nil

iex> FieldContent.new(%{key: "field4", value: "<a href='http://example.com'>Click here</a>", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false, number_style: "PKNumberStylePercent"})
%FieldContent{key: "field4", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false, label: nil, number_style: "PKNumberStylePercent", value: "<a href='http://example.com'>Click here</a>"}
iex> FieldContent.new(%{key: "field4", value: "<a href='http://example.com'>Click here</a>", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false, number_style: "PKNumberStylePercent", text_alignment: "PKTextAlignmentLeft"})
%FieldContent{key: "field4", attributed_value: nil, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false, label: nil, number_style: "PKNumberStylePercent", text_alignment: "PKTextAlignmentLeft", value: "<a href='http://example.com'>Click here</a>"}

iex> FieldContent.new(%{key: "field5", value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific"})
%FieldContent{key: "field5", attributed_value: nil, change_message: "Updated to %@", currency_code: nil, data_detector_types: [], date_style: nil, ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", value: "No detectors"}
iex> FieldContent.new(%{key: "field5", value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", text_alignment: "PKTextAlignmentNatural"})
%FieldContent{key: "field5", attributed_value: nil, change_message: "Updated to %@", currency_code: nil, data_detector_types: [], date_style: nil, ignores_time_zone: true, is_relative: true, label: "Field Label", number_style: "PKNumberStyleScientific", text_alignment: "PKTextAlignmentNatural", value: "No detectors"}
"""
@spec new(map()) :: %__MODULE__{}
def new(attrs \\ %{}) do
Expand All @@ -219,6 +239,7 @@ defmodule ExPass.Structs.FieldContent do
|> validate(:key, &Validators.validate_required_string(&1, :key))
|> validate(:label, &Validators.validate_optional_string(&1, :label))
|> validate(:number_style, &Validators.validate_number_style/1)
|> validate(:text_alignment, &Validators.validate_text_alignment/1)
|> validate(:value, &Validators.validate_required_value(&1, :value))

struct!(__MODULE__, attrs)
Expand Down
52 changes: 52 additions & 0 deletions lib/utils/validators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ defmodule ExPass.Utils.Validators do
"PKNumberStyleSpellOut"
]

@valid_text_alignments [
"PKTextAlignmentLeft",
"PKTextAlignmentCenter",
"PKTextAlignmentRight",
"PKTextAlignmentNatural"
]

@doc """
Validates the type of the attributed value.

Expand Down Expand Up @@ -488,6 +495,51 @@ defmodule ExPass.Utils.Validators do
def validate_required_value(_value, field_name),
do: {:error, "#{field_name} must be a string, number, DateTime, or Date"}

@doc """
Validates the text alignment value.

This function checks if the given value is a valid text alignment option.
Valid options are "PKTextAlignmentLeft", "PKTextAlignmentCenter", "PKTextAlignmentRight", and "PKTextAlignmentNatural".

## Parameters

* `value` - The text alignment value to validate.

## Returns

* `:ok` if the value is valid.
* `{:error, message}` if the value is invalid, where `message` is a string explaining the error.

## Examples

iex> validate_text_alignment("PKTextAlignmentLeft")
:ok

iex> validate_text_alignment("PKTextAlignmentCenter")
:ok

iex> validate_text_alignment("PKTextAlignmentRight")
:ok

iex> validate_text_alignment("PKTextAlignmentNatural")
:ok

iex> validate_text_alignment("InvalidAlignment")
{:error, "Invalid text_alignment. Supported values are PKTextAlignmentLeft, PKTextAlignmentCenter, PKTextAlignmentRight, PKTextAlignmentNatural"}

iex> validate_text_alignment(nil)
:ok

"""
@spec validate_text_alignment(String.t() | nil) :: :ok | {:error, String.t()}
def validate_text_alignment(nil), do: :ok
def validate_text_alignment(value) when value in @valid_text_alignments, do: :ok

def validate_text_alignment(_),
do:
{:error,
"Invalid text_alignment. Supported values are #{Enum.join(@valid_text_alignments, ", ")}"}

defp contains_unsupported_html_tags?(string) do
# Remove all valid anchor tags
string_without_anchors = String.replace(string, ~r{<a\s[^>]*>.*?</a>|<a\s[^>]*/>}, "")
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ defmodule ExPass.MixProject do
elixir: "~> 1.16",
config_path: "./config/config.exs",
start_permanent: Mix.env() == :prod,
deps: deps()
deps: deps(),
test_coverage: [threshold: 97.56]
]
end

Expand Down
95 changes: 95 additions & 0 deletions test/structs/field_content_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ defmodule ExPass.Structs.FieldContentTest do

doctest FieldContent

describe "new/1" do
test "new/1 raises ArgumentError when called with no arguments" do
assert_raise ArgumentError, "key is a required field and must be a non-empty string", fn ->
FieldContent.new()
end
end

test "creates a new FieldContent with minimum required fields" do
field_content = FieldContent.new(%{key: "test_key", value: "test_value"})
assert %FieldContent{key: "test_key", value: "test_value"} = field_content
end

test "raises ArgumentError for invalid attributes" do
assert_raise ArgumentError,
"Invalid data detector type: InvalidType. Supported types are: PKDataDetectorTypePhoneNumber, PKDataDetectorTypeLink, PKDataDetectorTypeAddress, PKDataDetectorTypeCalendarEvent",
fn ->
FieldContent.new(%{
key: "test",
value: "test",
data_detector_types: ["InvalidType"]
})
end
end
end

describe "change_message" do
test "new/1 raises ArgumentError for invalid change_message without '%@' placeholder" do
message = "Balance updated"
Expand Down Expand Up @@ -46,6 +71,24 @@ defmodule ExPass.Structs.FieldContentTest do
assert encoded =~ ~s("value":"test_value")
assert encoded =~ ~s("changeMessage":"Trimmed message %@")
end

test "validate_change_message/1 returns error for invalid change_message" do
invalid_message = "Invalid message without placeholder"

assert {:error,
"The change_message must be a string containing the '%@' placeholder for the new value."} ==
ExPass.Utils.Validators.validate_change_message(invalid_message)
end

test "validate_change_message/1 returns error when change_message is not a string" do
invalid_types = [42, :atom, [], %{}]

for invalid_type <- invalid_types do
assert {:error,
"The change_message must be a string containing the '%@' placeholder for the new value."} ==
ExPass.Utils.Validators.validate_change_message(invalid_type)
end
end
end

describe "attributed_value" do
Expand Down Expand Up @@ -547,6 +590,16 @@ defmodule ExPass.Structs.FieldContentTest do
assert encoded =~ ~s("numberStyle":"#{style}")
end)
end

test "new/1 raises ArgumentError when number_style is not a string" do
assert_raise ArgumentError, ~r/number_style must be a string/, fn ->
FieldContent.new(%{
key: "test_key",
value: "test_value",
number_style: :PKNumberStyleDecimal
})
end
end
end

describe "value" do
Expand Down Expand Up @@ -590,4 +643,46 @@ defmodule ExPass.Structs.FieldContentTest do
end
end
end

describe "text_alignment" do
test "new/1 creates a valid FieldContent struct with text_alignment" do
alignments = [
"PKTextAlignmentLeft",
"PKTextAlignmentCenter",
"PKTextAlignmentRight",
"PKTextAlignmentNatural"
]

Enum.each(alignments, fn alignment ->
result =
FieldContent.new(%{key: "test_key", value: "test_value", text_alignment: alignment})

assert %FieldContent{key: "test_key", value: "test_value", text_alignment: ^alignment} =
result

encoded = Jason.encode!(result)
assert encoded =~ ~s("key":"test_key")
assert encoded =~ ~s("value":"test_value")
assert encoded =~ ~s("textAlignment":"#{alignment}")
end)
end

test "new/1 raises ArgumentError for invalid text_alignment" do
assert_raise ArgumentError, ~r/Invalid text_alignment/, fn ->
FieldContent.new(%{
key: "test_key",
value: "test_value",
text_alignment: "InvalidAlignment"
})
end
end

test "new/1 allows text_alignment to be nil" do
result = FieldContent.new(%{key: "test_key", value: "test_value", text_alignment: nil})

assert %FieldContent{key: "test_key", value: "test_value", text_alignment: nil} = result
encoded = Jason.encode!(result)
refute encoded =~ "textAlignment"
end
end
end