Skip to content

Commit

Permalink
feat: added DateTimeField
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredhendrickson13 committed Nov 11, 2023
1 parent ad32932 commit b82a043
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 2 deletions.
4 changes: 2 additions & 2 deletions pfSense-pkg-API/files/etc/inc/api/core/Field.inc
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ class Field
$this->value = null;
return true;
}

# Ensure this field object has a name set
$this->check_field_names();

Expand Down Expand Up @@ -642,7 +642,7 @@ class Field
* @param mixed $value The value to check against assigned `type`.
* @throws ValidationError If the specified value is not a supported type.
*/
private function check_value_type(mixed $value) {
protected function check_value_type(mixed $value) {
# Don't validate type for `read_only` fields
if ($this->read_only) {
return;
Expand Down
160 changes: 160 additions & 0 deletions pfSense-pkg-API/files/etc/inc/api/fields/DateTimeField.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

namespace API\Fields;

require_once("api/auto_loader.inc");

use API;
use API\Core\Field;
use API\Responses\ValidationError;
use DateTime;

/**
* Defines a StringField class that extends the core Field method. This Field adds validation and representation for
* string Model fields.
*/
class DateTimeField extends Field {
public string $datetime_format;

/**
* Defines the StringField object and sets its options.
* @param bool $required If `true`, this field is required to have a value at all times.
* @param bool $unique If `true`, this field must be unique from all other parent model objects. Enabling this
* option requires the Model $context to be set AND the Model $context must have a `config_path` set.
* @param mixed|null $default Assign a default string value to assign this Field if no value is present.
* @param array $choices An array of value choices this Field can be assigned. This can either be an indexed array
* of the exact choice values, or an associative array where the array key is the exact choice value and the array
* value is a verbose name for the choice. Verbose choice name are used by ModelForms when generating web pages
* for a given Model.
* @param string $choices_callable Assign a callable method from this Field object OR the parent Model context to
* execute to populate choices for this field. This callable must be a method assigned on this Field object OR the
* parent Model object that returns an array of valid choices in the same format as $choices. This is helpful when
* choices are dynamic and must be populated at runtime instead of pre-determined sets of values.
* @param string $datetime_format the PHP DateTime format this value should be formatted as.
* https://www.php.net/manual/en/datetime.format.php
* @param bool $allow_empty If `true`, empty strings will be allowed by this field.
* @param bool $allow_null If `true`, null values will be allowed by this field.
* @param bool $editable Set to `false` to prevent this field's value from being changed after its initial creation.
* @param bool $read_only If `true`, this field can only read its value and cannot write its value to config.
* @param bool $write_only Set the `true` to make this field write-only. This will prevent the field's current value
* from being displayed in the representation data. This is ideal for potentially sensitive fields like passwords,
* keys, and hashes.
* @param bool $representation_only Set to `true` to make this field only present in its representation form. This
* effectively prevents the Field from being converted to an internal value which is saved to the pfSense config.
* This should only be used for fields that do not relate directly to a configuration value.
* @param bool $many If `true`, the value must be an array of many strings.
* @param int $many_minimum When $many is set to `true`, this sets the minimum number of array entries required.
* @param int $many_maximum When $many is set to `true`, this sets the maximum number of array entries allowed.
* @param string|null $delimiter Assigns the string delimiter to use when writing array values to config.
* Use `null` if this field is stored as an actual array in config. This is only available if $many is set to
* `true`. Defaults to `,` to store as comma-separated string.
* @param string $verbose_name The detailed name for this Field. This name will be used in non-programmatic areas
* like web pages and help text. This Field will default to property name assigned to the parent Model with
* underscores converted to spaces.
* @param string $verbose_name_plural The plural form of $verbose_name. This defaults to $verbose_name with `s`
* suffixed or `es` suffixes to strings already ending with `s`.
* @param string $internal_name Assign a different field name to use when referring to the internal field as it's
* stored in the pfSense configuration.
* @param string $internal_namespace Sets the namespace this field belongs to internally. This can be used to nest
* the fields internal value under a specific namespace as an associative array. This only applies to the internal
* value, not the representation value.
* @param array $referenced_by An array that specifies other Models and Field's that reference this Field's parent
* Model using this Field's value. This will prevent the parent Model object from being deleted while it is actively
* referenced by another Model object. The array key must be the name of the Model class that references this Field,
* and the value must be a Field within that Model. The framework will automatically search for any existing Model
* objects that have the referenced Field assigned a value that matches this Field's value.
* @param array $conditions An array of conditions the field must meet to be included. This allows you to specify
* conditions of other fields within the parent Model context. For example, if the parent Model context has two
* fields, one field named `type` and the other being this field; and you only want this field to be included if
* `type` is equal to `type1`, you could assign ["type" => "type1"] to this parameter.
* @param array $validators An array of Validator objects to run against this field.
* @param string $help_text Set a description for this field. This description will be used in API documentation.
*/
public function __construct(
bool $required = false,
bool $unique = false,
mixed $default = null,
array $choices = [],
string $choices_callable = "",
string $datetime_format = "m/d/Y",
bool $allow_empty = false,
bool $allow_null = false,
bool $editable = true,
bool $read_only = false,
bool $write_only = false,
bool $representation_only = false,
bool $many = false,
int $many_minimum = 1,
int $many_maximum = 128,
string|null $delimiter = ",",
string $verbose_name = "",
string $verbose_name_plural = "",
string $internal_name = "",
string $internal_namespace = "",
array $referenced_by = [],
array $conditions = [],
array $validators = [],
string $help_text = ""
)
{
$this->datetime_format = $datetime_format;
parent::__construct(
type: "string",
required: $required,
unique: $unique,
default: $default,
choices: $choices,
choices_callable: $choices_callable,
allow_empty: $allow_empty,
allow_null: $allow_null,
editable: $editable,
read_only: $read_only,
write_only: $write_only,
representation_only: $representation_only,
many: $many,
many_minimum: $many_minimum,
many_maximum: $many_maximum,
delimiter: $delimiter,
verbose_name: $verbose_name,
verbose_name_plural: $verbose_name_plural,
internal_name: $internal_name,
internal_namespace: $internal_namespace,
referenced_by: $referenced_by,
conditions: $conditions,
validators: $validators,
help_text: $help_text
);
}

/**
* @param string $datetime The datetime string to validate. (i.e. 12/31/1999)
* @param string $format The DateTime format of the $datetime string. (i.e. m/d/Y)
* @return bool `true` when the datetime string is valid, `false` when it is not.
*/
public static function is_valid_datetime(string $datetime, string $format) : bool {
# Try to create a DateTime object with this datetime string and format, then check for errors
$datetime_obj = DateTime::createFromFormat($format, $datetime);
$errors = DateTime::getLastErrors();

# This is not a valid datetime string if there are warnings
if ($errors["warning_count"]) {
return false;
}

return $datetime_obj !== false;
}

protected function check_value_type(mixed $value)
{
# Run the parent check_value_type() method to check primitive date type (i.e. string, boolean, integer)
parent::check_value_type($value);

# Also check to ensure that the datetime value matches this Field's DateTime format.
if (!$this->is_valid_datetime($value, $this->datetime_format)) {
throw new ValidationError(
message: "Field `$this->name` must be a DateTime in format `$this->datetime_format`. Received `$value`",
response_id: "DATETIME_FIELD_MUST_MATCH_FORMAT"
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace API\Tests;

use API\Core\TestCase;
use API\Fields\DateTimeField;

class APIFieldsDateTimeFieldTestCase extends TestCase
{
/**
* Checks that the is_valid_datetime() method correctly determines whether a datetime string matches a given format.
*/
public function test_is_valid_datetime() {
# Ensure valid datetimes return true
$this->assert_is_true(DateTimeField::is_valid_datetime("12/31/1999", "m/d/Y"));
$this->assert_is_true(DateTimeField::is_valid_datetime("Dec 31, 1999", "M d, Y"));

# Ensure invalid datetimes return false
$this->assert_is_false(DateTimeField::is_valid_datetime("13/31/1999", "m/d/Y"));
$this->assert_is_false(DateTimeField::is_valid_datetime("Test 31, 1999", "M d, Y"));
$this->assert_is_false(DateTimeField::is_valid_datetime("Dec 31, 1999", "m/d/Y"));
$this->assert_is_false(DateTimeField::is_valid_datetime("12/31/1999", "M d, Y"));
}

/**
* Checks that an error is thrown if this field is validated and its value does not match is datetime format
*/
public function test_validate() {
$this->assert_throws_response(
response_id: "DATETIME_FIELD_MUST_MATCH_FORMAT",
code: 400,
callable: function () {
$dt = new DateTimeField(required: true, datetime_format: "m/d/Y");
$dt->name = "test_field";
$dt->value = "not a datetime";
$dt->validate();
}
);

$this->assert_does_not_throw(
callable: function () {
$dt = new DateTimeField(required: true, datetime_format: "m/d/Y");
$dt->name = "test_field";
$dt->value = "12/31/1999";
$dt->validate();
}
);
}
}

0 comments on commit b82a043

Please sign in to comment.