Skip to content

Commit

Permalink
feat: Add data_detector_types attribute to FieldContent struct
Browse files Browse the repository at this point in the history
  • Loading branch information
njausteve committed Sep 21, 2024
1 parent 1adc1c8 commit ae5d58d
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 15 deletions.
57 changes: 42 additions & 15 deletions lib/structs/field_content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ defmodule ExPass.Structs.FieldContent do
- `attributed_value`: The field's value, which can include HTML markup for enhanced
formatting or interactivity. It can be a string, number, or ISO 8601 date string.
- `change_message`: A message that describes the change to the field's value.
It should include the '%@' placeholder for the new value.
- `currency_code`: The ISO 4217 currency code for the field's value, if applicable.
This is used when the field represents a monetary amount.
- `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.
Supported values are:
* "PKDataDetectorTypePhoneNumber" - Detects phone numbers
* "PKDataDetectorTypeLink" - Detects URLs and web links
* "PKDataDetectorTypeAddress" - Detects physical addresses
* "PKDataDetectorTypeCalendarEvent" - Detects calendar events
"""

use TypedStruct
Expand Down Expand Up @@ -49,18 +60,31 @@ defmodule ExPass.Structs.FieldContent do
"""
@type attributed_value() :: String.t() | number() | DateTime.t() | Date.t()

@typedoc """
The list of data detector types to apply to the field's value.
Optional. Valid values are:
- "PKDataDetectorTypePhoneNumber"
- "PKDataDetectorTypeLink"
- "PKDataDetectorTypeAddress"
- "PKDataDetectorTypeCalendarEvent"
These detectors can automatically convert certain types of data into tappable links.
"""
@type data_detector_types() :: list(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
end

@doc """
Creates a new FieldContent struct.
This function initializes a new FieldContent struct with the given attributes.
It validates both the `attributed_value` using the `ExPass.Utils.Validators.validate_attributed_value/1` function
and the `change_message` using the `ExPass.Utils.Validators.validate_change_message/1` function.
It validates the `attributed_value`, `change_message`, `currency_code`, and `data_detector_types`.
## Parameters
Expand All @@ -72,29 +96,24 @@ defmodule ExPass.Structs.FieldContent do
## Raises
* `ArgumentError` if the `attributed_value` is invalid. The error message will include details about the invalid value and supported types.
* `ArgumentError` if the `change_message` is invalid. The error message will include details about the invalid value and the required format.
* `ArgumentError` if any of the attributes are invalid. The error message will include details about the invalid value and supported types.
## Examples
iex> FieldContent.new(%{attributed_value: "Hello, World!"})
%FieldContent{attributed_value: "Hello, World!", change_message: nil}
%FieldContent{attributed_value: "Hello, World!", change_message: nil, currency_code: nil, data_detector_types: nil}
iex> FieldContent.new(%{attributed_value: 42})
%FieldContent{attributed_value: 42, change_message: 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> datetime = DateTime.utc_now()
iex> field_content = FieldContent.new(%{attributed_value: datetime})
iex> %FieldContent{attributed_value: ^datetime} = field_content
iex> field_content = FieldContent.new(%{attributed_value: datetime, currency_code: "USD"})
iex> %FieldContent{attributed_value: ^datetime, currency_code: "USD"} = field_content
iex> field_content.change_message
nil
iex> date = Date.utc_today()
iex> FieldContent.new(%{attributed_value: date})
%FieldContent{attributed_value: date, change_message: nil}
iex> FieldContent.new(%{attributed_value: "<a href='http://example.com'>Click here</a>"})
%FieldContent{attributed_value: "<a href='http://example.com'>Click here</a>", change_message: nil}
iex> FieldContent.new(%{attributed_value: "<a href='http://example.com'>Click here</a>", data_detector_types: ["PKDataDetectorTypeLink"]})
%FieldContent{attributed_value: "<a href='http://example.com'>Click here</a>", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"]}
"""
@spec new(map()) :: %__MODULE__{}
def new(attrs \\ %{}) do
Expand All @@ -104,6 +123,7 @@ defmodule ExPass.Structs.FieldContent do
|> validate(:attributed_value, &Validators.validate_attributed_value/1)
|> 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)

struct!(__MODULE__, attrs)
end
Expand All @@ -129,6 +149,13 @@ defmodule ExPass.Structs.FieldContent do
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])}
Expand Down
52 changes: 52 additions & 0 deletions lib/utils/validators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ defmodule ExPass.Utils.Validators do

@currency_code_regex ~r/^(AED|AFN|ALL|AMD|ANG|AOA|ARS|AUD|AWG|AZN|BAM|BBD|BDT|BGN|BHD|BIF|BMD|BND|BOB|BOV|BRL|BSD|BTN|BWP|BYN|BZD|CAD|CDF|CHE|CHF|CHW|CLF|CLP|CNY|COP|COU|CRC|CUP|CVE|CZK|DJF|DKK|DOP|DZD|EGP|ERN|ETB|EUR|FJD|FKP|GBP|GEL|GHS|GIP|GMD|GNF|GTQ|GYD|HKD|HNL|HTG|HUF|IDR|ILS|INR|IQD|IRR|ISK|JMD|JOD|JPY|KES|KGS|KHR|KMF|KPW|KRW|KWD|KYD|KZT|LAK|LBP|LKR|LRD|LSL|LYD|MAD|MDL|MGA|MKD|MMK|MNT|MOP|MRU|MUR|MVR|MWK|MXN|MXV|MYR|MZN|NAD|NGN|NIO|NOK|NPR|NZD|OMR|PAB|PEN|PGK|PHP|PKR|PLN|PYG|QAR|RON|RSD|RUB|RWF|SAR|SBD|SCR|SDG|SEK|SGD|SHP|SLE|SOS|SRD|SSP|STN|SVC|SYP|SZL|THB|TJS|TMT|TND|TOP|TRY|TTD|TWD|TZS|UAH|UGX|USD|USN|UYI|UYU|UYW|UZS|VED|VES|VND|VUV|WST|XAF|XAG|XAU|XBA|XBB|XBC|XBD|XCD|XDR|XOF|XPD|XPF|XPT|XSU|XTS|XUA|XXX|YER|ZAR|ZMW|ZWG|ZWL)$/

@valid_detector_types [
"PKDataDetectorTypePhoneNumber",
"PKDataDetectorTypeLink",
"PKDataDetectorTypeAddress",
"PKDataDetectorTypeCalendarEvent"
]

@doc """
Validates the type of the attributed value.
Expand Down Expand Up @@ -151,6 +158,51 @@ defmodule ExPass.Utils.Validators do

def validate_currency_code(_), do: {:error, "Currency code must be a string or atom"}

@doc """
Validates the data_detector_types field.
The data_detector_types must be a list of valid detector type strings.
## Returns
* `:ok` if the value is a valid list of detector types or nil.
* `{:error, reason}` if the value is not valid, where reason is a string explaining the error.
## Examples
iex> validate_data_detector_types(["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"])
:ok
iex> validate_data_detector_types([])
:ok
iex> validate_data_detector_types(nil)
:ok
iex> validate_data_detector_types(["InvalidDetector"])
{:error, "Invalid data detector type: InvalidDetector. Supported types are: PKDataDetectorTypePhoneNumber, PKDataDetectorTypeLink, PKDataDetectorTypeAddress, PKDataDetectorTypeCalendarEvent"}
iex> validate_data_detector_types("PKDataDetectorTypePhoneNumber")
{:error, "data_detector_types must be a list"}
"""
@spec validate_data_detector_types(list(String.t()) | nil) :: :ok | {:error, String.t()}
def validate_data_detector_types(nil), do: :ok
def validate_data_detector_types([]), do: :ok

def validate_data_detector_types(types) when is_list(types) do
invalid_types = Enum.reject(types, &(&1 in @valid_detector_types))

if Enum.empty?(invalid_types) do
:ok
else
{:error,
"Invalid data detector type: #{Enum.join(invalid_types, ", ")}. Supported types are: #{Enum.join(@valid_detector_types, ", ")}"}
end
end

def validate_data_detector_types(_), do: {:error, "data_detector_types must be a list"}

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
58 changes: 58 additions & 0 deletions test/structs/field_content_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,62 @@ defmodule ExPass.Structs.FieldContentTest do
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 [email protected]",
data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"]
}

result = FieldContent.new(input)

assert %FieldContent{
attributed_value: "Contact us at [email protected]",
data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"]
} = result

assert Jason.encode!(result) ==
~s({"attributedValue":"Contact us at [email protected]","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)

assert %FieldContent{attributed_value: "No detectors", 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"})
end

test "new/1 raises ArgumentError for invalid data_detector_types" do
assert_raise ArgumentError,
~r/Invalid data detector type: InvalidDetector. Supported types are: PKDataDetectorTypePhoneNumber, PKDataDetectorTypeLink, PKDataDetectorTypeAddress, PKDataDetectorTypeCalendarEvent/,
fn ->
FieldContent.new(%{
attributed_value: "Invalid",
data_detector_types: ["InvalidDetector"]
})
end
end

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
end

0 comments on commit ae5d58d

Please sign in to comment.