Skip to content

Commit

Permalink
Merge pull request #160 from njausteve/add-number-style-pass-field-co…
Browse files Browse the repository at this point in the history
…ntent-field

add number style pass field content field
  • Loading branch information
njausteve authored Sep 24, 2024
2 parents b5d46d8 + 699e2ac commit 115d2f4
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 74 deletions.
74 changes: 29 additions & 45 deletions lib/structs/field_content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ defmodule ExPass.Structs.FieldContent do
- `key`: A unique key that identifies a field in the pass. This field is required.
- `label`: The text for a field label. This field is optional.
- `number_style`: The style of the number to display in the field.
Supported values are:
* "PKNumberStyleDecimal"
* "PKNumberStylePercent"
* "PKNumberStyleScientific"
* "PKNumberStyleSpellOut"
"""

use TypedStruct
Expand Down Expand Up @@ -108,6 +115,17 @@ defmodule ExPass.Structs.FieldContent do
"""
@type date_style() :: String.t()

@typedoc """
The style of the number to display in the field.
Optional. Valid values are:
- "PKNumberStyleDecimal"
- "PKNumberStylePercent"
- "PKNumberStyleScientific"
- "PKNumberStyleSpellOut"
"""
@type number_style() :: String.t()

typedstruct do
field :attributed_value, attributed_value(), default: nil
field :change_message, String.t(), default: nil
Expand All @@ -118,6 +136,7 @@ defmodule ExPass.Structs.FieldContent do
field :is_relative, boolean(), default: nil
field :key, String.t(), enforce: true
field :label, String.t(), default: nil
field :number_style, number_style(), default: nil
end

@doc """
Expand All @@ -134,6 +153,7 @@ defmodule ExPass.Structs.FieldContent do
• is_relative
• key
• label
• number_style
## Parameters
Expand All @@ -150,22 +170,22 @@ defmodule ExPass.Structs.FieldContent do
## Examples
iex> FieldContent.new(%{key: "field1", attributed_value: "Hello, World!"})
%FieldContent{key: "field1", attributed_value: "Hello, World!", change_message: nil, currency_code: nil, data_detector_types: nil, date_style: nil, ignores_time_zone: nil, is_relative: nil, label: nil}
%FieldContent{key: "field1", attributed_value: "Hello, World!", 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}
iex> FieldContent.new(%{key: "field2", attributed_value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false})
%FieldContent{key: "field2", attributed_value: 42, change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, label: nil}
iex> FieldContent.new(%{key: "field2", attributed_value: 42, data_detector_types: ["PKDataDetectorTypePhoneNumber"], date_style: "PKDateStyleShort", ignores_time_zone: true, is_relative: false, number_style: "PKNumberStyleDecimal"})
%FieldContent{key: "field2", attributed_value: 42, 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"}
iex> datetime = ~U[2023-04-15 14:30:00Z]
iex> field_content = FieldContent.new(%{key: "field3", attributed_value: datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true})
iex> %FieldContent{key: "field3", attributed_value: ^datetime, currency_code: "USD", date_style: "PKDateStyleLong", ignores_time_zone: true, is_relative: true} = field_content
iex> field_content.change_message
nil
iex> FieldContent.new(%{key: "field4", attributed_value: "<a href='http://example.com'>Click here</a>", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false})
%FieldContent{key: "field4", attributed_value: "<a href='http://example.com'>Click here</a>", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false, label: nil}
iex> FieldContent.new(%{key: "field4", attributed_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: "<a href='http://example.com'>Click here</a>", 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"}
iex> FieldContent.new(%{key: "field5", attributed_value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true, label: "Field Label"})
%FieldContent{key: "field5", attributed_value: "No detectors", change_message: "Updated to %@", currency_code: nil, data_detector_types: [], date_style: nil, ignores_time_zone: true, is_relative: true, label: "Field Label"}
iex> FieldContent.new(%{key: "field5", attributed_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: "No detectors", 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"}
"""
@spec new(map()) :: %__MODULE__{}
def new(attrs \\ %{}) do
Expand All @@ -181,6 +201,7 @@ defmodule ExPass.Structs.FieldContent do
|> validate(:is_relative, &Validators.validate_boolean_field(&1, :is_relative))
|> validate(:key, &Validators.validate_required_string(&1, :key))
|> validate(:label, &Validators.validate_optional_string(&1, :label))
|> validate(:number_style, &Validators.validate_number_style/1)

struct!(__MODULE__, attrs)
end
Expand All @@ -191,47 +212,10 @@ defmodule ExPass.Structs.FieldContent do
attrs

{:error, reason} ->
error_message = get_error_message(key, attrs[key], reason)
raise ArgumentError, error_message
raise ArgumentError, reason
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 <a></a> 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. Use an empty list to disable all detectors."

:date_style ->
"Supported values are: PKDateStyleNone, PKDateStyleShort, PKDateStyleMedium, PKDateStyleLong, PKDateStyleFull"

key when key in [:ignores_time_zone, :is_relative] ->
"#{key} must be a boolean value (true or false)"

:key ->
"key is a required field and must be a non-empty string"

:label ->
"label must be a string if provided"

_ ->
""
end

base_message <> additional_info
end

defimpl Jason.Encoder do
def encode(field_content, opts) do
field_content
Expand Down
99 changes: 81 additions & 18 deletions lib/utils/validators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ defmodule ExPass.Utils.Validators do
"PKDateStyleFull"
]

@valid_number_styles [
"PKNumberStyleDecimal",
"PKNumberStylePercent",
"PKNumberStyleScientific",
"PKNumberStyleSpellOut"
]

@doc """
Validates the type of the attributed value.
Expand All @@ -39,7 +46,7 @@ defmodule ExPass.Utils.Validators do
## Returns
* `:ok` if the value is of a valid type.
* `{:error, "invalid attributed_value type"}` if the value is not of a valid type.
* `{:error, reason}` if the value is not of a valid type, where reason is a string explaining the error.
## Examples
Expand All @@ -56,7 +63,7 @@ defmodule ExPass.Utils.Validators do
:ok
iex> validate_attributed_value(%{})
{:error, "invalid attributed_value type"}
{:error, "Invalid attributed_value type. Supported types are: String (including <a></a> tag), number, DateTime and Date"}
"""
@spec validate_attributed_value(String.t() | number() | DateTime.t() | Date.t() | nil) ::
Expand All @@ -66,8 +73,11 @@ defmodule ExPass.Utils.Validators do

def validate_attributed_value(value) when is_binary(value) do
case contains_unsupported_html_tags?(value) do
true -> {:error, "contains unsupported HTML tags"}
false -> :ok
true ->
{:error, "Supported types are: String (including <a></a> tag), number, DateTime and Date"}

false ->
:ok
end
end

Expand All @@ -83,7 +93,10 @@ defmodule ExPass.Utils.Validators do
:ok
end

def validate_attributed_value(_), do: {:error, "invalid attributed_value type"}
def validate_attributed_value(_),
do:
{:error,
"Invalid attributed_value type. Supported types are: String (including <a></a> tag), number, DateTime and Date"}

@doc """
Validates the change_message field.
Expand All @@ -101,13 +114,13 @@ defmodule ExPass.Utils.Validators do
:ok
iex> validate_change_message("Invalid message without placeholder")
{:error, "change_message must contain '%@' placeholder"}
{:error, "The change_message must be a string containing the '%@' placeholder for the new value."}
iex> validate_change_message(nil)
:ok
iex> validate_change_message(42)
{:error, "change_message must be a string"}
{:error, "The change_message must be a string containing the '%@' placeholder for the new value."}
"""
@spec validate_change_message(String.t() | nil) :: :ok | {:error, String.t()}
Expand All @@ -117,11 +130,15 @@ defmodule ExPass.Utils.Validators do
if String.contains?(value, "%@") do
:ok
else
{:error, "change_message must contain '%@' placeholder"}
{:error,
"The change_message must be a string containing the '%@' placeholder for the new value."}
end
end

def validate_change_message(_), do: {:error, "change_message must be a string"}
def validate_change_message(_),
do:
{:error,
"The change_message must be a string containing the '%@' placeholder for the new value."}

@doc """
Validates the currency_code field.
Expand All @@ -142,7 +159,7 @@ defmodule ExPass.Utils.Validators do
:ok
iex> validate_currency_code("INVALID")
{:error, "Invalid currency code"}
{:error, "Invalid currency code INVALID"}
iex> validate_currency_code(nil)
:ok
Expand Down Expand Up @@ -277,13 +294,15 @@ defmodule ExPass.Utils.Validators do
:ok
iex> validate_boolean_field("true", :is_relative)
{:error, "is_relative must be a boolean"}
{:error, "is_relative must be a boolean value (true or false)"}
"""
@spec validate_boolean_field(boolean() | nil, atom()) :: :ok | {:error, String.t()}
def validate_boolean_field(nil, _field_name), do: :ok
def validate_boolean_field(value, _field_name) when is_boolean(value), do: :ok
def validate_boolean_field(_, field_name), do: {:error, "#{field_name} must be a boolean"}

def validate_boolean_field(_, field_name),
do: {:error, "#{field_name} must be a boolean value (true or false)"}

@doc """
Validates a required string field.
Expand All @@ -309,20 +328,23 @@ defmodule ExPass.Utils.Validators do
{:error, "key cannot be an empty string"}
iex> validate_required_string(nil, :key)
{:error, "key is required"}
{:error, "key is a required field and must be a non-empty string"}
iex> validate_required_string(123, :key)
{:error, "key must be a string"}
{:error, "key is a required field and must be a non-empty string"}
"""
@spec validate_required_string(String.t() | nil, atom()) :: :ok | {:error, String.t()}
def validate_required_string(nil, field_name), do: {:error, "#{field_name} is required"}
def validate_required_string(nil, field_name),
do: {:error, "#{field_name} is a required field and must be a non-empty string"}

def validate_required_string("", field_name),
do: {:error, "#{field_name} cannot be an empty string"}

def validate_required_string(value, _field_name) when is_binary(value), do: :ok
def validate_required_string(_, field_name), do: {:error, "#{field_name} must be a string"}

def validate_required_string(_, field_name),
do: {:error, "#{field_name} is a required field and must be a non-empty string"}

@doc """
Validates an optional string field.
Expand Down Expand Up @@ -351,13 +373,54 @@ defmodule ExPass.Utils.Validators do
:ok
iex> validate_optional_string(123, :label)
{:error, "label must be a string"}
{:error, "label must be a string if provided"}
"""
@spec validate_optional_string(String.t() | nil, atom()) :: :ok | {:error, String.t()}
def validate_optional_string(nil, _field_name), do: :ok
def validate_optional_string(value, _field_name) when is_binary(value), do: :ok
def validate_optional_string(_, field_name), do: {:error, "#{field_name} must be a string"}

def validate_optional_string(_, field_name),
do: {:error, "#{field_name} must be a string if provided"}

@doc """
Validates the number_style field.
The number_style must be a valid number style string.
## Returns
* `:ok` if the value is a valid number style string or nil.
* `{:error, reason}` if the value is not valid, where reason is a string explaining the error.
## Examples
iex> validate_number_style("PKNumberStyleDecimal")
:ok
iex> validate_number_style(nil)
:ok
iex> validate_number_style("InvalidStyle")
{:error, "Invalid number_style: InvalidStyle. Supported values are: PKNumberStyleDecimal, PKNumberStylePercent, PKNumberStyleScientific, PKNumberStyleSpellOut"}
iex> validate_number_style(42)
{:error, "number_style must be a string"}
"""
@spec validate_number_style(String.t() | nil) :: :ok | {:error, String.t()}
def validate_number_style(nil), do: :ok

def validate_number_style(value) when is_binary(value) do
if value in @valid_number_styles do
:ok
else
{:error,
"Invalid number_style: #{value}. Supported values are: #{Enum.join(@valid_number_styles, ", ")}"}
end
end

def validate_number_style(_), do: {:error, "number_style must be a string"}

defp contains_unsupported_html_tags?(string) do
# Remove all valid anchor tags
Expand Down
Loading

0 comments on commit 115d2f4

Please sign in to comment.