diff --git a/lib/structs/field_content.ex b/lib/structs/field_content.ex
index 0cc6d4e..96d1d52 100644
--- a/lib/structs/field_content.ex
+++ b/lib/structs/field_content.ex
@@ -46,6 +46,8 @@ defmodule ExPass.Structs.FieldContent do
This key doesn't affect the pass relevance calculation.
- `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.
"""
use TypedStruct
@@ -115,6 +117,7 @@ defmodule ExPass.Structs.FieldContent do
field :ignores_time_zone, boolean(), default: nil
field :is_relative, boolean(), default: nil
field :key, String.t(), enforce: true
+ field :label, String.t(), default: nil
end
@doc """
@@ -130,6 +133,7 @@ defmodule ExPass.Structs.FieldContent do
• ignores_time_zone
• is_relative
• key
+ • label
## Parameters
@@ -146,10 +150,10 @@ 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}
+ %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}
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}
+ %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> 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})
@@ -158,10 +162,10 @@ defmodule ExPass.Structs.FieldContent do
nil
iex> FieldContent.new(%{key: "field4", attributed_value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false})
- %FieldContent{key: "field4", attributed_value: "Click here", change_message: nil, currency_code: nil, data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", ignores_time_zone: nil, is_relative: false}
+ %FieldContent{key: "field4", attributed_value: "Click here", 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: "field5", attributed_value: "No detectors", data_detector_types: [], change_message: "Updated to %@", ignores_time_zone: true, is_relative: true})
- %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}
+ 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"}
"""
@spec new(map()) :: %__MODULE__{}
def new(attrs \\ %{}) do
@@ -176,6 +180,7 @@ defmodule ExPass.Structs.FieldContent do
|> validate(:ignores_time_zone, &Validators.validate_boolean_field(&1, :ignores_time_zone))
|> 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))
struct!(__MODULE__, attrs)
end
@@ -217,6 +222,9 @@ defmodule ExPass.Structs.FieldContent do
:key ->
"key is a required field and must be a non-empty string"
+ :label ->
+ "label must be a string if provided"
+
_ ->
""
end
diff --git a/lib/utils/validators.ex b/lib/utils/validators.ex
index 3657efe..1183c2b 100644
--- a/lib/utils/validators.ex
+++ b/lib/utils/validators.ex
@@ -324,6 +324,41 @@ defmodule ExPass.Utils.Validators do
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"}
+ @doc """
+ Validates an optional string field.
+
+ The field must be a string or nil.
+
+ ## Parameters
+
+ * `value` - The value to validate.
+ * `field_name` - The name of the field being validated as an atom.
+
+ ## Returns
+
+ * `:ok` if the value is a valid string or nil.
+ * `{:error, reason}` if the value is not valid, where reason is a string explaining the error.
+
+ ## Examples
+
+ iex> validate_optional_string("valid string", :label)
+ :ok
+
+ iex> validate_optional_string("", :label)
+ :ok
+
+ iex> validate_optional_string(nil, :label)
+ :ok
+
+ iex> validate_optional_string(123, :label)
+ {:error, "label must be a string"}
+
+ """
+ @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"}
+
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 d7aee51..5370663 100644
--- a/test/structs/field_content_test.exs
+++ b/test/structs/field_content_test.exs
@@ -363,4 +363,48 @@ defmodule ExPass.Structs.FieldContentTest do
assert encoded =~ ~s("key":"trimmed_key")
end
end
+
+ describe "label" do
+ test "new/1 creates a valid FieldContent struct with a label" do
+ result = FieldContent.new(%{key: "test_key", label: "Test Label"})
+
+ assert %FieldContent{key: "test_key", label: "Test Label"} = result
+ encoded = Jason.encode!(result)
+ assert encoded =~ ~s("key":"test_key")
+ assert encoded =~ ~s("label":"Test Label")
+ end
+
+ test "new/1 creates a valid FieldContent struct without a label" do
+ result = FieldContent.new(%{key: "test_key"})
+
+ assert %FieldContent{key: "test_key", label: nil} = result
+ encoded = Jason.encode!(result)
+ assert encoded =~ ~s("key":"test_key")
+ refute encoded =~ "label"
+ end
+
+ test "new/1 trims whitespace from label" do
+ result = FieldContent.new(%{key: "test_key", label: " Trimmed Label "})
+
+ assert %FieldContent{key: "test_key", label: "Trimmed Label"} = result
+ encoded = Jason.encode!(result)
+ assert encoded =~ ~s("key":"test_key")
+ assert encoded =~ ~s("label":"Trimmed Label")
+ end
+
+ test "new/1 raises ArgumentError when label is not a string" do
+ assert_raise ArgumentError, ~r/label must be a string/, fn ->
+ FieldContent.new(%{key: "test_key", label: 123})
+ end
+ end
+
+ test "new/1 allows an empty string for label" do
+ result = FieldContent.new(%{key: "test_key", label: ""})
+
+ assert %FieldContent{key: "test_key", label: ""} = result
+ encoded = Jason.encode!(result)
+ assert encoded =~ ~s("key":"test_key")
+ assert encoded =~ ~s("label":"")
+ end
+ end
end