diff --git a/lib/structs/field_content.ex b/lib/structs/field_content.ex index 19c101c..a5deb99 100644 --- a/lib/structs/field_content.ex +++ b/lib/structs/field_content.ex @@ -55,6 +55,9 @@ defmodule ExPass.Structs.FieldContent do * "PKNumberStylePercent" * "PKNumberStyleScientific" * "PKNumberStyleSpellOut" + + - `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. """ use TypedStruct @@ -126,6 +129,18 @@ defmodule ExPass.Structs.FieldContent do """ @type number_style() :: String.t() + @typedoc """ + The value to use for the field. + + Required. Can be: + - A localizable string (e.g., "Hello, World!") + - An ISO 8601 date string (e.g., "2023-04-15T14:30:00Z") + - A number (e.g., 42) + + A date or time value must include a time zone. + """ + @type value() :: String.t() | number() | DateTime.t() | Date.t() + typedstruct do field :attributed_value, attributed_value(), default: nil field :change_message, String.t(), default: nil @@ -137,6 +152,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 :value, value(), enforce: true end @doc """ @@ -154,6 +170,7 @@ defmodule ExPass.Structs.FieldContent do • key • label • number_style + • value ## Parameters @@ -169,23 +186,23 @@ 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, number_style: nil} + 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!"} - 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> 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> 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 = 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.change_message nil - iex> FieldContent.new(%{key: "field4", attributed_value: "Click here", data_detector_types: ["PKDataDetectorTypeLink"], date_style: "PKDateStyleFull", is_relative: false, number_style: "PKNumberStylePercent"}) - %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, number_style: "PKNumberStylePercent"} + iex> FieldContent.new(%{key: "field4", value: "Click here", 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: "Click here"} - 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"} + 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"} """ @spec new(map()) :: %__MODULE__{} def new(attrs \\ %{}) do @@ -202,6 +219,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(:value, &Validators.validate_required_value(&1, :value)) struct!(__MODULE__, attrs) end diff --git a/lib/utils/validators.ex b/lib/utils/validators.ex index fe7f35a..6c2751e 100644 --- a/lib/utils/validators.ex +++ b/lib/utils/validators.ex @@ -422,6 +422,72 @@ defmodule ExPass.Utils.Validators do def validate_number_style(_), do: {:error, "number_style must be a string"} + @doc """ + Validates a required value field. + + The field must be a non-nil value. The value can be a localizable string, ISO 8601 date, or number. + + ## Parameters + + * `value` - The value to validate. A date or time value must include a time zone. + * `field_name` - The name of the field being validated as an atom. + + ## Returns + + * `:ok` if the value is valid (non-nil). + * `{:error, reason}` if the value is not valid, where reason is a string explaining the error. + + ## Examples + + iex> validate_required_value(42, :value) + :ok + + iex> validate_required_value(nil, :value) + {:error, "value is a required field and cannot be nil"} + + iex> validate_required_value("2021-09-15T15:53:00Z", :value) + :ok + + iex> validate_required_value("localizable string", :value) + :ok + + iex> validate_required_value(nil, :another_field) + {:error, "value is a required field and cannot be nil"} + + iex> validate_required_value("2023-04-15T14:30:00", :value) + {:error, "Date value must include a time zone"} + + """ + @spec validate_required_value(String.t() | number() | DateTime.t() | Date.t() | nil, atom()) :: + :ok | {:error, String.t()} + def validate_required_value(nil, field_name), + do: {:error, "#{field_name} is a required field and cannot be nil"} + + def validate_required_value(value, _field_name) when is_binary(value) do + if String.contains?(value, "T") and not String.contains?(value, "Z") do + {:error, "Date value must include a time zone"} + else + :ok + end + end + + def validate_required_value(value, _field_name) when is_number(value), do: :ok + + def validate_required_value(%DateTime{} = value, _field_name) do + DateTime.to_iso8601(value) + + :ok + end + + def validate_required_value(%Date{} = value, _field_name) do + Date.to_iso8601(value) + + :ok + end + + def validate_required_value(_value, field_name), + do: {:error, "#{field_name} must be a string, number, DateTime, or Date"} + 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 345b582..e0531bc 100644 --- a/test/structs/field_content_test.exs +++ b/test/structs/field_content_test.exs @@ -13,59 +13,74 @@ defmodule ExPass.Structs.FieldContentTest do assert_raise ArgumentError, "The change_message must be a string containing the '%@' placeholder for the new value.", fn -> - FieldContent.new(%{key: "test_key", change_message: message}) + FieldContent.new(%{ + key: "test_key", + value: "test_value", + change_message: message + }) end end test "new/1 creates a FieldContent struct with valid change_message containing '%@' placeholder" do message = "Balance updated to %@" - result = FieldContent.new(%{key: "test_key", change_message: message}) + result = FieldContent.new(%{key: "test_key", value: "test_value", change_message: message}) assert result.change_message == message assert result.key == "test_key" + assert result.value == "test_value" encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("changeMessage":"Balance updated to %@") end test "new/1 trims whitespace from change_message while preserving '%@' placeholder" do message = " Trimmed message %@ " - result = FieldContent.new(%{key: "test_key", change_message: message}) + result = FieldContent.new(%{key: "test_key", value: "test_value", change_message: message}) assert result.change_message == "Trimmed message %@" assert result.key == "test_key" + assert result.value == "test_value" encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("changeMessage":"Trimmed message %@") end end describe "attributed_value" do test "new/1 creates an empty FieldContent struct when no attributes are provided" do - result = FieldContent.new(%{key: "test_key"}) - assert %FieldContent{attributed_value: nil} = result + result = FieldContent.new(%{key: "test_key", value: "test_value"}) + assert %FieldContent{attributed_value: nil, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") refute encoded =~ "attributedValue" end test "new/1 creates a valid FieldContent struct with string" do input_string = "Hello, World!" - result = FieldContent.new(%{key: "test_key", attributed_value: input_string}) - assert %FieldContent{attributed_value: ^input_string} = result + result = + FieldContent.new(%{key: "test_key", value: "test_value", attributed_value: input_string}) + + assert %FieldContent{attributed_value: ^input_string, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("attributedValue":"Hello, World!") end test "new/1 creates a valid FieldContent struct with number" do input_number = 42 - result = FieldContent.new(%{key: "test_key", attributed_value: input_number}) - assert %FieldContent{attributed_value: ^input_number} = result + result = + FieldContent.new(%{key: "test_key", value: "test_value", attributed_value: input_number}) + + assert %FieldContent{attributed_value: ^input_number, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("attributedValue":42) end @@ -76,28 +91,38 @@ defmodule ExPass.Structs.FieldContentTest do assert_raise ArgumentError, "Invalid attributed_value type. Supported types are: String (including tag), number, DateTime and Date", fn -> - FieldContent.new(%{key: "test_key", attributed_value: invalid_value}) + FieldContent.new(%{ + key: "test_key", + value: "test_value", + attributed_value: invalid_value + }) end end end test "new/1 creates a valid FieldContent struct with DateTime" do input_time = DateTime.utc_now() - result = FieldContent.new(%{key: "test_key", attributed_value: input_time}) - assert %FieldContent{attributed_value: ^input_time} = result + result = + FieldContent.new(%{key: "test_key", value: "test_value", attributed_value: input_time}) + + assert %FieldContent{attributed_value: ^input_time, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("attributedValue":"#{DateTime.to_iso8601(input_time)}") end test "new/1 creates a valid FieldContent struct with Date" do input_date = Date.utc_today() - result = FieldContent.new(%{key: "test_key", attributed_value: input_date}) - assert %FieldContent{attributed_value: ^input_date} = result + result = + FieldContent.new(%{key: "test_key", value: "test_value", attributed_value: input_date}) + + assert %FieldContent{attributed_value: ^input_date, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("attributedValue":"#{Date.to_iso8601(input_date)}") end @@ -107,62 +132,72 @@ defmodule ExPass.Structs.FieldContentTest do assert_raise ArgumentError, "Supported types are: String (including tag), number, DateTime and Date", fn -> - FieldContent.new(%{key: "test_key", attributed_value: input_value}) + FieldContent.new(%{ + key: "test_key", + value: "test_value", + attributed_value: input_value + }) end end test "new/1 creates a valid FieldContent struct with supported HTML tag" do input_value = "Link" - result = FieldContent.new(%{key: "test_key", attributed_value: input_value}) - assert %FieldContent{attributed_value: ^input_value} = result + result = + FieldContent.new(%{key: "test_key", value: "test_value", attributed_value: input_value}) + + assert %FieldContent{attributed_value: ^input_value, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("attributedValue":"Link") end end describe "currency_code" do test "new/1 creates a valid FieldContent struct with valid currency_code as string" do - result = FieldContent.new(%{key: "test_key", currency_code: "USD"}) + result = FieldContent.new(%{key: "test_key", value: "test_value", currency_code: "USD"}) - assert %FieldContent{currency_code: "USD"} = result + assert %FieldContent{currency_code: "USD", value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("currencyCode":"USD") end test "new/1 creates a valid FieldContent struct with valid currency_code as atom" do - result = FieldContent.new(%{key: "test_key", currency_code: :USD}) + result = FieldContent.new(%{key: "test_key", value: "test_value", currency_code: :USD}) - assert %FieldContent{currency_code: :USD} = result + assert %FieldContent{currency_code: :USD, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~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(%{key: "test_key", currency_code: "INVALID"}) + FieldContent.new(%{key: "test_key", value: "test_value", currency_code: "INVALID"}) end assert_raise ArgumentError, ~r/Invalid currency code INVALID/, fn -> - FieldContent.new(%{key: "test_key", currency_code: :INVALID}) + FieldContent.new(%{key: "test_key", value: "test_value", currency_code: :INVALID}) end 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(%{key: "test_key", currency_code: 123}) + FieldContent.new(%{key: "test_key", value: "test_value", currency_code: 123}) end end test "new/1 trims whitespace from currency_code string" do - result = FieldContent.new(%{key: "test_key", currency_code: " USD "}) + result = FieldContent.new(%{key: "test_key", value: "test_value", currency_code: " USD "}) - assert %FieldContent{currency_code: "USD"} = result + assert %FieldContent{currency_code: "USD", value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("currencyCode":"USD") end end @@ -172,27 +207,31 @@ defmodule ExPass.Structs.FieldContentTest do result = FieldContent.new(%{ key: "test_key", + value: "test_value", data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"] }) assert %FieldContent{ - data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"] + data_detector_types: ["PKDataDetectorTypePhoneNumber", "PKDataDetectorTypeLink"], + value: "test_value" } = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("dataDetectorTypes":["PKDataDetectorTypePhoneNumber","PKDataDetectorTypeLink"]) end test "new/1 creates a valid FieldContent struct with empty data_detector_types" do - result = FieldContent.new(%{key: "test_key", data_detector_types: []}) + result = FieldContent.new(%{key: "test_key", value: "test_value", data_detector_types: []}) - assert %FieldContent{data_detector_types: []} = result + assert %FieldContent{data_detector_types: [], value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("dataDetectorTypes":[]) end @@ -202,6 +241,7 @@ defmodule ExPass.Structs.FieldContentTest do fn -> FieldContent.new(%{ key: "test_key", + value: "test_value", data_detector_types: ["InvalidDetector"] }) end @@ -211,6 +251,7 @@ defmodule ExPass.Structs.FieldContentTest do assert_raise ArgumentError, ~r/data_detector_types must be a list/, fn -> FieldContent.new(%{ key: "test_key", + value: "test_value", data_detector_types: "PKDataDetectorTypePhoneNumber" }) end @@ -219,12 +260,14 @@ defmodule ExPass.Structs.FieldContentTest do describe "date_style" do test "new/1 creates a valid FieldContent struct with date_style" do - result = FieldContent.new(%{key: "test_key", date_style: "PKDateStyleShort"}) + result = + FieldContent.new(%{key: "test_key", value: "test_value", date_style: "PKDateStyleShort"}) - assert %FieldContent{date_style: "PKDateStyleShort"} = result + assert %FieldContent{date_style: "PKDateStyleShort", value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("dateStyle":"PKDateStyleShort") end @@ -238,10 +281,11 @@ defmodule ExPass.Structs.FieldContentTest do ] for style <- valid_styles do - result = FieldContent.new(%{key: "test_key", date_style: style}) - assert %FieldContent{date_style: ^style} = result + result = FieldContent.new(%{key: "test_key", value: "test_value", date_style: style}) + assert %FieldContent{date_style: ^style, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("dateStyle":"#{style}") end end @@ -252,6 +296,7 @@ defmodule ExPass.Structs.FieldContentTest do fn -> FieldContent.new(%{ key: "test_key", + value: "test_value", date_style: "InvalidStyle" }) end @@ -259,183 +304,228 @@ defmodule ExPass.Structs.FieldContentTest 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(%{key: "test_key", date_style: :PKDateStyleShort}) + FieldContent.new(%{key: "test_key", value: "test_value", date_style: :PKDateStyleShort}) end end end describe "ignores_time_zone" do test "new/1 creates a valid FieldContent struct with ignores_time_zone set to true" do - result = FieldContent.new(%{key: "test_key", ignores_time_zone: true}) + result = FieldContent.new(%{key: "test_key", value: "test_value", ignores_time_zone: true}) - assert %FieldContent{ignores_time_zone: true} = result + assert %FieldContent{ignores_time_zone: true, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("ignoresTimeZone":true) end test "new/1 creates a valid FieldContent struct with ignores_time_zone set to false" do - result = FieldContent.new(%{key: "test_key", ignores_time_zone: false}) + result = FieldContent.new(%{key: "test_key", value: "test_value", ignores_time_zone: false}) - assert %FieldContent{ignores_time_zone: false} = result + assert %FieldContent{ignores_time_zone: false, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("ignoresTimeZone":false) end test "new/1 defaults to nil when ignores_time_zone is not provided" do - result = FieldContent.new(%{key: "test_key"}) + result = FieldContent.new(%{key: "test_key", value: "test_value"}) - assert %FieldContent{ignores_time_zone: nil} = result + assert %FieldContent{ignores_time_zone: nil, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") refute encoded =~ "ignoresTimeZone" end test "new/1 raises ArgumentError when ignores_time_zone is not a boolean" do assert_raise ArgumentError, ~r/ignores_time_zone must be a boolean/, fn -> - FieldContent.new(%{key: "test_key", ignores_time_zone: "true"}) + FieldContent.new(%{key: "test_key", value: "test_value", ignores_time_zone: "true"}) end end end describe "is_relative" do test "new/1 creates a valid FieldContent struct with is_relative set to true" do - result = FieldContent.new(%{key: "test_key", is_relative: true}) + result = FieldContent.new(%{key: "test_key", value: "test_value", is_relative: true}) - assert %FieldContent{is_relative: true} = result + assert %FieldContent{is_relative: true, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("isRelative":true) end test "new/1 creates a valid FieldContent struct with is_relative set to false" do - result = FieldContent.new(%{key: "test_key", is_relative: false}) + result = FieldContent.new(%{key: "test_key", value: "test_value", is_relative: false}) - assert %FieldContent{is_relative: false} = result + assert %FieldContent{is_relative: false, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("isRelative":false) end test "new/1 defaults to nil when is_relative is not provided" do - result = FieldContent.new(%{key: "test_key"}) + result = FieldContent.new(%{key: "test_key", value: "test_value"}) - assert %FieldContent{is_relative: nil} = result + assert %FieldContent{is_relative: nil, value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") refute encoded =~ "isRelative" end test "new/1 raises ArgumentError when is_relative is not a boolean" do assert_raise ArgumentError, ~r/is_relative must be a boolean/, fn -> - FieldContent.new(%{key: "test_key", is_relative: "true"}) + FieldContent.new(%{key: "test_key", value: "test_value", is_relative: "true"}) end end end - describe "key" do - test "new/1 creates a valid FieldContent struct with a key" do - result = FieldContent.new(%{key: "unique_identifier"}) + describe "key and value" do + test "new/1 creates a valid FieldContent struct with a key and value" do + result = FieldContent.new(%{key: "unique_identifier", value: "test_value"}) - assert %FieldContent{key: "unique_identifier"} = result + assert %FieldContent{key: "unique_identifier", value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"unique_identifier") + assert encoded =~ ~s("value":"test_value") end test "new/1 raises ArgumentError when key is not provided" do assert_raise ArgumentError, "key is a required field and must be a non-empty string", fn -> - FieldContent.new(%{}) + FieldContent.new(%{value: "test_value"}) + end + end + + test "new/1 raises ArgumentError when value is not provided" do + assert_raise ArgumentError, "value is a required field and cannot be nil", fn -> + FieldContent.new(%{key: "unique_identifier"}) end end test "new/1 raises ArgumentError when key is an empty string" do assert_raise ArgumentError, ~r/key cannot be an empty string/, fn -> - FieldContent.new(%{key: ""}) + FieldContent.new(%{key: "", value: "test_value"}) end end test "new/1 raises ArgumentError when key is not a string" do assert_raise ArgumentError, "key is a required field and must be a non-empty string", fn -> - FieldContent.new(%{key: 123}) + FieldContent.new(%{key: 123, value: "test_value"}) end end test "new/1 trims whitespace from key" do - result = FieldContent.new(%{key: " trimmed_key "}) + result = FieldContent.new(%{key: " trimmed_key ", value: "test_value"}) - assert %FieldContent{key: "trimmed_key"} = result + assert %FieldContent{key: "trimmed_key", value: "test_value"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"trimmed_key") + assert encoded =~ ~s("value":"test_value") + end + + test "new/1 allows various types for value" do + string_value = FieldContent.new(%{key: "string_key", value: "string_value"}) + assert %FieldContent{key: "string_key", value: "string_value"} = string_value + + number_value = FieldContent.new(%{key: "number_key", value: 42}) + assert %FieldContent{key: "number_key", value: 42} = number_value + + date_value = FieldContent.new(%{key: "date_key", value: ~D[2023-05-17]}) + assert %FieldContent{key: "date_key", value: ~D[2023-05-17]} = date_value + + datetime_value = FieldContent.new(%{key: "datetime_key", value: ~U[2023-05-17 10:00:00Z]}) + assert %FieldContent{key: "datetime_key", value: ~U[2023-05-17 10:00:00Z]} = datetime_value 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"}) + result = FieldContent.new(%{key: "test_key", value: "test_value", label: "Test Label"}) - assert %FieldContent{key: "test_key", label: "Test Label"} = result + assert %FieldContent{key: "test_key", value: "test_value", label: "Test Label"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") 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"}) + result = FieldContent.new(%{key: "test_key", value: "test_value"}) - assert %FieldContent{key: "test_key", label: nil} = result + assert %FieldContent{key: "test_key", value: "test_value", label: nil} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") refute encoded =~ "label" end test "new/1 trims whitespace from label" do - result = FieldContent.new(%{key: "test_key", label: " Trimmed Label "}) + result = + FieldContent.new(%{key: "test_key", value: "test_value", label: " Trimmed Label "}) - assert %FieldContent{key: "test_key", label: "Trimmed Label"} = result + assert %FieldContent{key: "test_key", value: "test_value", label: "Trimmed Label"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") 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}) + FieldContent.new(%{key: "test_key", value: "test_value", label: 123}) end end test "new/1 allows an empty string for label" do - result = FieldContent.new(%{key: "test_key", label: ""}) + result = FieldContent.new(%{key: "test_key", value: "test_value", label: ""}) - assert %FieldContent{key: "test_key", label: ""} = result + assert %FieldContent{key: "test_key", value: "test_value", label: ""} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("label":"") end end describe "number_style" do test "new/1 creates a valid FieldContent struct with number_style" do - result = FieldContent.new(%{key: "test_key", number_style: "PKNumberStyleDecimal"}) + result = + FieldContent.new(%{ + key: "test_key", + value: "test_value", + number_style: "PKNumberStyleDecimal" + }) + + assert %FieldContent{ + key: "test_key", + value: "test_value", + number_style: "PKNumberStyleDecimal" + } = result - assert %FieldContent{key: "test_key", number_style: "PKNumberStyleDecimal"} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("numberStyle":"PKNumberStyleDecimal") end test "new/1 raises ArgumentError for invalid number_style" do assert_raise ArgumentError, ~r/Invalid number_style/, fn -> - FieldContent.new(%{key: "test_key", number_style: "InvalidStyle"}) + FieldContent.new(%{key: "test_key", value: "test_value", number_style: "InvalidStyle"}) end end test "new/1 allows nil for number_style" do - result = FieldContent.new(%{key: "test_key", number_style: nil}) + result = FieldContent.new(%{key: "test_key", value: "test_value", number_style: nil}) - assert %FieldContent{key: "test_key", number_style: nil} = result + assert %FieldContent{key: "test_key", value: "test_value", number_style: nil} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") refute encoded =~ "numberStyle" end @@ -448,13 +538,56 @@ defmodule ExPass.Structs.FieldContentTest do ] Enum.each(styles, fn style -> - result = FieldContent.new(%{key: "test_key", number_style: style}) + result = FieldContent.new(%{key: "test_key", value: "test_value", number_style: style}) - assert %FieldContent{key: "test_key", number_style: ^style} = result + assert %FieldContent{key: "test_key", value: "test_value", number_style: ^style} = result encoded = Jason.encode!(result) assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"test_value") assert encoded =~ ~s("numberStyle":"#{style}") end) end end + + describe "value" do + test "new/1 creates a valid FieldContent struct with a localizable string value" do + result = FieldContent.new(%{key: "test_key", value: "Hello, World!"}) + + assert %FieldContent{key: "test_key", value: "Hello, World!"} = result + encoded = Jason.encode!(result) + assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"Hello, World!") + end + + test "new/1 creates a valid FieldContent struct with an ISO 8601 date value" do + iso_date = "2023-04-15T14:30:00Z" + result = FieldContent.new(%{key: "test_key", value: iso_date}) + + assert %FieldContent{key: "test_key", value: ^iso_date} = result + encoded = Jason.encode!(result) + assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":"2023-04-15T14:30:00Z") + end + + test "new/1 creates a valid FieldContent struct with a number value" do + result = FieldContent.new(%{key: "test_key", value: 42}) + + assert %FieldContent{key: "test_key", value: 42} = result + encoded = Jason.encode!(result) + assert encoded =~ ~s("key":"test_key") + assert encoded =~ ~s("value":42) + end + + test "new/1 raises ArgumentError for invalid value type" do + assert_raise ArgumentError, "value must be a string, number, DateTime, or Date", fn -> + FieldContent.new(%{key: "test_key", value: %{invalid: "type"}}) + end + end + + test "new/1 raises ArgumentError for missing time zone in date value" do + assert_raise ArgumentError, ~r/Date value must include a time zone/, fn -> + FieldContent.new(%{key: "test_key", value: "2023-04-15T14:30:00"}) + end + end + end end