From f7ce27df5004aafd80adb3041a30cb4b8dc938e1 Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 21 Sep 2024 20:43:45 +0800 Subject: [PATCH 1/3] feat: Add validation for date_style field in FieldContent struct This commit adds a new validation function, `validate_date_style`, to the `ExPass.Utils.Validators` module. The function validates the `date_style` field in the `FieldContent` struct, ensuring that it is a valid date style string or nil. If the value is not valid, an error is returned with a message explaining the error. This validation enhances the reliability and correctness of handling date styles in the application. --- lib/structs/field_content.ex | 95 ++++++++++++++++++----------- lib/utils/validators.ex | 47 ++++++++++++++ test/structs/field_content_test.exs | 51 ++++++++++++++++ 3 files changed, 157 insertions(+), 36 deletions(-) diff --git a/lib/structs/field_content.ex b/lib/structs/field_content.ex index 2c91313..cf5ad39 100644 --- a/lib/structs/field_content.ex +++ b/lib/structs/field_content.ex @@ -26,6 +26,14 @@ defmodule ExPass.Structs.FieldContent do * "PKDataDetectorTypeLink" - Detects URLs and web links * "PKDataDetectorTypeAddress" - Detects physical addresses * "PKDataDetectorTypeCalendarEvent" - Detects calendar events + + - `date_style`: The style of the date to display in the field. + Supported values are: + * "PKDateStyleNone" + * "PKDateStyleShort" + * "PKDateStyleMedium" + * "PKDateStyleLong" + * "PKDateStyleFull" """ use TypedStruct @@ -73,18 +81,31 @@ defmodule ExPass.Structs.FieldContent do """ @type data_detector_types() :: list(String.t()) + @typedoc """ + The style of the date to display in the field. + + Optional. Valid values are: + - "PKDateStyleNone" + - "PKDateStyleShort" + - "PKDateStyleMedium" + - "PKDateStyleLong" + - "PKDateStyleFull" + """ + @type date_style() :: String.t() + typedstruct do field :attributed_value, attributed_value(), default: nil field :change_message, String.t(), default: nil field :currency_code, String.t(), default: nil field :data_detector_types, data_detector_types(), default: nil + field :date_style, date_style(), default: nil end @doc """ Creates a new FieldContent struct. This function initializes a new FieldContent struct with the given attributes. - It validates the `attributed_value`, `change_message`, `currency_code`, and `data_detector_types`. + It validates the `attributed_value`, `change_message`, `currency_code`, `data_detector_types`, and `date_style`. ## Parameters @@ -101,19 +122,19 @@ defmodule ExPass.Structs.FieldContent do ## Examples iex> FieldContent.new(%{attributed_value: "Hello, World!"}) - %FieldContent{attributed_value: "Hello, World!", change_message: nil, currency_code: nil, data_detector_types: nil} + %FieldContent{attributed_value: "Hello, World!", change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil} - iex> FieldContent.new(%{attributed_value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"]}) - %FieldContent{attributed_value: 42, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"]} + iex> FieldContent.new(%{attributed_value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort"}) + %FieldContent{attributed_value: 42, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort"} iex> datetime = DateTime.utc_now() - iex> field_content = FieldContent.new(%{attributed_value: datetime, currency_code: "USD"}) - iex> %FieldContent{attributed_value: ^datetime, currency_code: "USD"} = field_content + iex> field_content = FieldContent.new(%{attributed_value: datetime, currency_code: "USD", date_style: "PKDateStyleLong"}) + iex> %FieldContent{attributed_value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong"} = field_content iex> field_content.change_message nil - iex> FieldContent.new(%{attributed_value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"]}) - %FieldContent{attributed_value: "Click here", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"]} + iex> FieldContent.new(%{attributed_value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull"}) + %FieldContent{attributed_value: "Click here", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull"} """ @spec new(map()) :: %__MODULE__{} def new(attrs \\ %{}) do @@ -124,6 +145,7 @@ defmodule ExPass.Structs.FieldContent do |> validate(:change_message, &Validators.validate_change_message/1) |> validate(:currency_code, &Validators.validate_currency_code/1) |> validate(:data_detector_types, &Validators.validate_data_detector_types/1) + |> validate(:date_style, &Validators.validate_date_style/1) struct!(__MODULE__, attrs) end @@ -134,37 +156,38 @@ defmodule ExPass.Structs.FieldContent do attrs {:error, reason} -> - cond do - key == :attributed_value -> - raise ArgumentError, """ - Invalid attributed_value: #{inspect(attrs[key])} - Reason: #{reason} - Supported types are: String (including tag), number, DateTime and Date - """ - - key == :change_message -> - raise ArgumentError, """ - Invalid change_message: #{inspect(attrs[key])} - Reason: #{reason} - The change_message must be a string containing the '%@' placeholder for the new value. - """ - - key == :data_detector_types -> - raise ArgumentError, """ - Invalid data_detector_types: #{inspect(attrs[key])} - Reason: #{reason} - data_detector_types must be a list of valid detector type strings. - """ - - true -> - raise ArgumentError, """ - Invalid value for #{key}: #{inspect(attrs[key])} - Reason: #{reason} - """ - end + error_message = get_error_message(key, attrs[key], reason) + raise ArgumentError, error_message end end + defp get_error_message(key, value, reason) do + base_message = """ + Invalid #{key}: #{inspect(value)} + Reason: #{reason} + """ + + additional_info = + case key do + :attributed_value -> + "Supported types are: String (including tag), number, DateTime and Date" + + :change_message -> + "The change_message must be a string containing the '%@' placeholder for the new value." + + :data_detector_types -> + "data_detector_types must be a list of valid detector type strings." + + :date_style -> + "Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull" + + _ -> + "" + end + + base_message <> additional_info + end + defimpl Jason.Encoder do def encode(field_content, opts) do field_content diff --git a/lib/utils/validators.ex b/lib/utils/validators.ex index 14de9b1..6e8dec7 100644 --- a/lib/utils/validators.ex +++ b/lib/utils/validators.ex @@ -18,6 +18,14 @@ defmodule ExPass.Utils.Validators do "PKDataDetectorTypeCalendarEvent" ] + @valid_date_styles [ + "PKDateStyleNone", + "PKDateStyleShort", + "PKDateStyleMedium", + "PKDateStyleLong", + "PKDateStyleFull" + ] + @doc """ Validates the type of the attributed value. @@ -203,6 +211,45 @@ defmodule ExPass.Utils.Validators do def validate_data_detector_types(_), do: {:error, "data_detector_types must be a list"} + @doc """ + Validates the date_style field. + + The date_style must be a valid date style string. + + ## Returns + + * `:ok` if the value is a valid date style string or nil. + * `{:error, reason}` if the value is not valid, where reason is a string explaining the error. + + ## Examples + + iex> validate_date_style("PKDateStyleShort") + :ok + + iex> validate_date_style(nil) + :ok + + iex> validate_date_style("InvalidStyle") + {:error, "Invalid date_style: InvalidStyle. Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull"} + + iex> validate_date_style(42) + {:error, "date_style must be a string"} + + """ + @spec validate_date_style(String.t() | nil) :: :ok | {:error, String.t()} + def validate_date_style(nil), do: :ok + + def validate_date_style(style) when is_binary(style) do + if style in @valid_date_styles do + :ok + else + {:error, + "Invalid date_style: #{style}. Supported values are: #{Enum.join(@valid_date_styles, ", ")}"} + end + end + + def validate_date_style(_), do: {:error, "date_style must be a string"} + defp contains_unsupported_html_tags?(string) do # Remove all valid anchor tags string_without_anchors = String.replace(string, ~r{]*>.*?|]*/>}, "") diff --git a/test/structs/field_content_test.exs b/test/structs/field_content_test.exs index 9e9124a..0f3ecd7 100644 --- a/test/structs/field_content_test.exs +++ b/test/structs/field_content_test.exs @@ -204,4 +204,55 @@ defmodule ExPass.Structs.FieldContentTest do end end end + + describe "dateStyle" do + test "new/1 creates a valid FieldContent struct with dateStyle" do + input = %{attributed_value: "2023-05-01", date_style: "PKDateStyleShort"} + result = FieldContent.new(input) + + assert %FieldContent{attributed_value: "2023-05-01", date_style: "PKDateStyleShort"} = + result + + assert Jason.encode!(result) == + ~s({"attributedValue":"2023-05-01","dateStyle":"PKDateStyleShort"}) + end + + test "new/1 allows all valid dateStyle values" do + valid_styles = [ + "PKDateStyleNone", + "PKDateStyleShort", + "PKDateStyleMedium", + "PKDateStyleLong", + "PKDateStyleFull" + ] + + for style <- valid_styles do + result = FieldContent.new(%{attributed_value: "2023-05-01", date_style: style}) + assert %FieldContent{date_style: ^style} = result + end + end + + test "new/1 raises ArgumentError for invalid dateStyle" do + assert_raise ArgumentError, + ~r/Invalid date_style: InvalidStyle. Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull/, + fn -> + FieldContent.new(%{ + attributed_value: "2023-05-01", + date_style: "InvalidStyle" + }) + end + end + + test "new/1 allows nil dateStyle" do + result = FieldContent.new(%{attributed_value: "2023-05-01"}) + assert %FieldContent{attributed_value: "2023-05-01", date_style: nil} = result + assert Jason.encode!(result) == ~s({"attributedValue":"2023-05-01"}) + end + + test "new/1 raises ArgumentError when dateStyle is not a string" do + assert_raise ArgumentError, ~r/date_style must be a string/, fn -> + FieldContent.new(%{attributed_value: "2023-05-01", date_style: :PKDateStyleShort}) + end + end + end end From 6fecc8c9a460992ffcc79926271f51d7e17013c2 Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 21 Sep 2024 20:52:27 +0800 Subject: [PATCH 2/3] feat: Add support for specifying data detectors in FieldContent struct This commit adds the `data_detector_types` attribute to the `FieldContent` struct in the `ExPass.Structs.FieldContent` module. This attribute allows for specifying a list of data detectors to apply to the field's value, which can automatically convert certain types of data into tappable links. By default, all data detectors are applied. To use no data detectors, an empty list can be specified. This enhancement improves the flexibility and functionality of handling field content in the application. --- lib/structs/field_content.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/structs/field_content.ex b/lib/structs/field_content.ex index cf5ad39..ca7fb89 100644 --- a/lib/structs/field_content.ex +++ b/lib/structs/field_content.ex @@ -20,6 +20,7 @@ defmodule ExPass.Structs.FieldContent do - `data_detector_types`: A list of data detectors to apply to the field's value. These detectors can automatically convert certain types of data into tappable links. + By default, all data detectors are applied. To use no data detectors, specify an empty list. Supported values are: * "PKDataDetectorTypePhoneNumber" - Detects phone numbers @@ -78,8 +79,9 @@ defmodule ExPass.Structs.FieldContent do - "PKDataDetectorTypeCalendarEvent" These detectors can automatically convert certain types of data into tappable links. + By default, all data detectors are applied. To use no data detectors, specify an empty list. """ - @type data_detector_types() :: list(String.t()) + @type data_detector_types() :: list(String.t()) | [] @typedoc """ The style of the date to display in the field. @@ -135,6 +137,9 @@ defmodule ExPass.Structs.FieldContent do iex> FieldContent.new(%{attributed_value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull"}) %FieldContent{attributed_value: "Click here", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull"} + + iex> FieldContent.new(%{attributed_value: "No detectors", data_detector_types: []}) + %FieldContent{attributed_value: "No detectors", change_message: nil, currency_code: nil, data_detector_types: [], date_style: nil} """ @spec new(map()) :: %__MODULE__{} def new(attrs \\ %{}) do @@ -176,7 +181,7 @@ defmodule ExPass.Structs.FieldContent do "The change_message must be a string containing the '%@' placeholder for the new value." :data_detector_types -> - "data_detector_types must be a list of valid detector type strings." + "data_detector_types must be a list of valid detector type strings. Use an empty list to disable all detectors." :date_style -> "Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull" From a6124cc2f8e0b750f1d2f760c2098fd02af1378d Mon Sep 17 00:00:00 2001 From: njausteve Date: Sat, 21 Sep 2024 21:02:24 +0800 Subject: [PATCH 3/3] test: only test single fields --- test/structs/field_content_test.exs | 95 +++++++++-------------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/test/structs/field_content_test.exs b/test/structs/field_content_test.exs index 0f3ecd7..d9baa56 100644 --- a/test/structs/field_content_test.exs +++ b/test/structs/field_content_test.exs @@ -6,7 +6,7 @@ defmodule ExPass.Structs.FieldContentTest do doctest FieldContent - describe "FieldContent struct change_message" do + describe "change_message" do test "new/1 raises ArgumentError for invalid change_message without '%@' placeholder" do message = "Balance updated" @@ -32,7 +32,7 @@ defmodule ExPass.Structs.FieldContentTest do end end - describe "FieldContent struct attributed_value" do + describe "attributed_value" do test "new/1 creates an empty FieldContent struct when no attributes are provided" do assert %FieldContent{attributed_value: nil} = FieldContent.new() assert Jason.encode!(FieldContent.new()) == ~s({}) @@ -101,87 +101,64 @@ defmodule ExPass.Structs.FieldContentTest do describe "currency_code" do test "new/1 creates a valid FieldContent struct with valid currency_code as string" do - input = %{attributed_value: 100, currency_code: "USD"} - result = FieldContent.new(input) + result = FieldContent.new(%{currency_code: "USD"}) - assert %FieldContent{attributed_value: 100, currency_code: "USD"} = result - assert Jason.encode!(result) == ~s({"attributedValue":100,"currencyCode":"USD"}) + assert %FieldContent{currency_code: "USD"} = result + assert Jason.encode!(result) == ~s({"currencyCode":"USD"}) end test "new/1 creates a valid FieldContent struct with valid currency_code as atom" do - input = %{attributed_value: 100, currency_code: :USD} - result = FieldContent.new(input) + result = FieldContent.new(%{currency_code: :USD}) - assert %FieldContent{attributed_value: 100, currency_code: :USD} = result - assert Jason.encode!(result) == ~s({"attributedValue":100,"currencyCode":"USD"}) + assert %FieldContent{currency_code: :USD} = result + assert Jason.encode!(result) == ~s({"currencyCode":"USD"}) end test "new/1 raises ArgumentError for invalid currency_code" do assert_raise ArgumentError, ~r/Invalid currency code INVALID/, fn -> - FieldContent.new(%{attributed_value: 100, currency_code: "INVALID"}) + FieldContent.new(%{currency_code: "INVALID"}) end assert_raise ArgumentError, ~r/Invalid currency code INVALID/, fn -> - FieldContent.new(%{attributed_value: 100, currency_code: :INVALID}) + FieldContent.new(%{currency_code: :INVALID}) end end - test "new/1 allows nil currency_code" do - result = FieldContent.new(%{attributed_value: 100}) - - assert %FieldContent{attributed_value: 100, currency_code: nil} = result - assert Jason.encode!(result) == ~s({"attributedValue":100}) - end - test "new/1 raises ArgumentError when currency_code is not a string or atom" do assert_raise ArgumentError, ~r/Currency code must be a string or atom/, fn -> - FieldContent.new(%{attributed_value: 100, currency_code: 123}) + FieldContent.new(%{currency_code: 123}) end end test "new/1 trims whitespace from currency_code string" do result = FieldContent.new(%{currency_code: " USD "}) - assert %FieldContent{attributed_value: nil, currency_code: "USD"} = result + assert %FieldContent{currency_code: "USD"} = result assert Jason.encode!(result) == ~s({"currencyCode":"USD"}) end end describe "data_detector_types" do test "new/1 creates a valid FieldContent struct with valid data_detector_types" do - input = %{ - attributed_value: "Contact us at info@example.com", - data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"] - } - - result = FieldContent.new(input) + result = + FieldContent.new(%{ + data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"] + }) assert %FieldContent{ - attributed_value: "Contact us at info@example.com", data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"] } = result assert Jason.encode!(result) == - ~s({"attributedValue":"Contact us at info@example.com","dataDetectorTypes":["PKDataDetectorTypePhoneNumber","PKDataDetectorTypeLink"]}) + ~s({"dataDetectorTypes":["PKDataDetectorTypePhoneNumber","PKDataDetectorTypeLink"]}) end test "new/1 creates a valid FieldContent struct with empty data_detector_types" do - input = %{attributed_value: "No detectors", data_detector_types: []} - result = FieldContent.new(input) + result = FieldContent.new(%{data_detector_types: []}) - assert %FieldContent{attributed_value: "No detectors", data_detector_types: []} = result + assert %FieldContent{data_detector_types: []} = result - assert Jason.encode!(result) == - ~s({"attributedValue":"No detectors","dataDetectorTypes":[]}) - end - - test "new/1 allows nil data_detector_types" do - result = FieldContent.new(%{attributed_value: "Default detectors"}) - - assert %FieldContent{attributed_value: "Default detectors", data_detector_types: nil} = - result - - assert Jason.encode!(result) == ~s({"attributedValue":"Default detectors"}) + assert Jason.encode!(result) == ~s({"dataDetectorTypes":[]}) end test "new/1 raises ArgumentError for invalid data_detector_types" do @@ -189,7 +166,6 @@ defmodule ExPass.Structs.FieldContentTest do ~r/Invalid data detector type: InvalidDetector. Supported types are: PKDataDetectorTypePhoneNumber, PKDataDetectorTypeLink, PKDataDetectorTypeAddress, PKDataDetectorTypeCalendarEvent/, fn -> FieldContent.new(%{ - attributed_value: "Invalid", data_detector_types: ["InvalidDetector"] }) end @@ -198,26 +174,22 @@ defmodule ExPass.Structs.FieldContentTest do test "new/1 raises ArgumentError when data_detector_types is not a list" do assert_raise ArgumentError, ~r/data_detector_types must be a list/, fn -> FieldContent.new(%{ - attributed_value: "Invalid", data_detector_types: "PKDataDetectorTypePhoneNumber" }) end end end - describe "dateStyle" do - test "new/1 creates a valid FieldContent struct with dateStyle" do - input = %{attributed_value: "2023-05-01", date_style: "PKDateStyleShort"} - result = FieldContent.new(input) + describe "date_style" do + test "new/1 creates a valid FieldContent struct with date_style" do + result = FieldContent.new(%{date_style: "PKDateStyleShort"}) - assert %FieldContent{attributed_value: "2023-05-01", date_style: "PKDateStyleShort"} = - result + assert %FieldContent{date_style: "PKDateStyleShort"} = result - assert Jason.encode!(result) == - ~s({"attributedValue":"2023-05-01","dateStyle":"PKDateStyleShort"}) + assert Jason.encode!(result) == ~s({"dateStyle":"PKDateStyleShort"}) end - test "new/1 allows all valid dateStyle values" do + test "new/1 allows all valid date_style values" do valid_styles = [ "PKDateStyleNone", "PKDateStyleShort", @@ -227,31 +199,24 @@ defmodule ExPass.Structs.FieldContentTest do ] for style <- valid_styles do - result = FieldContent.new(%{attributed_value: "2023-05-01", date_style: style}) + result = FieldContent.new(%{date_style: style}) assert %FieldContent{date_style: ^style} = result end end - test "new/1 raises ArgumentError for invalid dateStyle" do + test "new/1 raises ArgumentError for invalid date_style" do assert_raise ArgumentError, ~r/Invalid date_style: InvalidStyle. Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull/, fn -> FieldContent.new(%{ - attributed_value: "2023-05-01", date_style: "InvalidStyle" }) end end - test "new/1 allows nil dateStyle" do - result = FieldContent.new(%{attributed_value: "2023-05-01"}) - assert %FieldContent{attributed_value: "2023-05-01", date_style: nil} = result - assert Jason.encode!(result) == ~s({"attributedValue":"2023-05-01"}) - end - - test "new/1 raises ArgumentError when dateStyle is not a string" do + test "new/1 raises ArgumentError when date_style is not a string" do assert_raise ArgumentError, ~r/date_style must be a string/, fn -> - FieldContent.new(%{attributed_value: "2023-05-01", date_style: :PKDateStyleShort}) + FieldContent.new(%{date_style: :PKDateStyleShort}) end end end