MINIFICPP-2862 Add StandardValidator::NUMBER_VALIDATOR (#2213)
diff --git a/.github/references/ubuntu_22_04_clang_arm_manifest.json b/.github/references/ubuntu_22_04_clang_arm_manifest.json
index bcb495d..750f395 100644
--- a/.github/references/ubuntu_22_04_clang_arm_manifest.json
+++ b/.github/references/ubuntu_22_04_clang_arm_manifest.json
@@ -10268,7 +10268,7 @@
                     "Min P": {
                         "name": "Min P",
                         "description": "Sets a minimum base probability threshold for token selection. 0.0 = disabled.",
-                        "validator": "VALID",
+                        "validator": "NUMBER_VALIDATOR",
                         "required": "false",
                         "sensitive": "false",
                         "expressionLanguageScope": "NONE"
diff --git a/core-framework/common/include/utils/ParsingUtils.h b/core-framework/common/include/utils/ParsingUtils.h
index 33236ed..85e18bd 100644
--- a/core-framework/common/include/utils/ParsingUtils.h
+++ b/core-framework/common/include/utils/ParsingUtils.h
@@ -40,7 +40,8 @@
 
 std::expected<uint32_t, std::error_code> parseUnixOctalPermissions(std::string_view input);
 
-std::expected<float, std::error_code> parseFloat(std::string_view input);
+template<std::floating_point T>
+std::expected<T, std::error_code> parseFloatingPoint(std::string_view input);
 
 template<std::integral T>
 std::expected<T, std::error_code> parseIntegralMinMax(const std::string_view input, const T minimum, const T maximum) {
@@ -90,4 +91,22 @@
   return *result;
 }
 
+template<std::floating_point T>
+std::expected<T, std::error_code> parseFloatingPoint(const std::string_view input) {
+  const auto trimmed_input = utils::string::trim(input);
+  T value{};
+
+  const auto [ptr, ec] = std::from_chars(trimmed_input.data(), trimmed_input.data() + trimmed_input.size(), value);
+
+  if (ec != std::errc()) {
+    return std::unexpected{core::ParsingErrorCode::GeneralParsingError};
+  }
+
+  if (ptr != trimmed_input.data() + trimmed_input.size()) {
+    return std::unexpected{core::ParsingErrorCode::GeneralParsingError};
+  }
+
+  return value;
+}
+
 }  // namespace org::apache::nifi::minifi::parsing
diff --git a/core-framework/common/src/utils/ParsingUtils.cpp b/core-framework/common/src/utils/ParsingUtils.cpp
index 922704b..75f095e 100644
--- a/core-framework/common/src/utils/ParsingUtils.cpp
+++ b/core-framework/common/src/utils/ParsingUtils.cpp
@@ -126,12 +126,4 @@
   return result;
 }
 
-std::expected<float, std::error_code> parseFloat(std::string_view input) {
-  try {
-    return std::stof(std::string{input});
-  } catch(const std::exception&) {
-    return std::unexpected{core::ParsingErrorCode::GeneralParsingError};
-  }
-}
-
 }  // namespace org::apache::nifi::minifi::parsing
diff --git a/extension-framework/cpp-extension-lib/include/api/utils/ProcessorConfigUtils.h b/extension-framework/cpp-extension-lib/include/api/utils/ProcessorConfigUtils.h
index 2ec9571..ea2c17d 100644
--- a/extension-framework/cpp-extension-lib/include/api/utils/ProcessorConfigUtils.h
+++ b/extension-framework/cpp-extension-lib/include/api/utils/ProcessorConfigUtils.h
@@ -134,7 +134,7 @@
     if (property_str->empty()) {
       return std::nullopt;
     }
-    return parsing::parseFloat(*property_str)
+    return parsing::parseFloatingPoint<float>(*property_str)
         | minifi::utils::orThrow(fmt::format("Expected parsable float from \"{}\"", property.name));
   }
   return std::nullopt;
diff --git a/extension-framework/cpp-extension-lib/src/utils/minifi-c-utils.cpp b/extension-framework/cpp-extension-lib/src/utils/minifi-c-utils.cpp
index e11fd89..cf70d2c 100644
--- a/extension-framework/cpp-extension-lib/src/utils/minifi-c-utils.cpp
+++ b/extension-framework/cpp-extension-lib/src/utils/minifi-c-utils.cpp
@@ -55,6 +55,9 @@
   if (validator->getEquivalentNifiStandardValidatorName() == minifi::core::StandardPropertyValidators::PORT_VALIDATOR.getEquivalentNifiStandardValidatorName()) {
     return MINIFI_VALIDATOR_PORT;
   }
+  if (validator->getEquivalentNifiStandardValidatorName() == minifi::core::StandardPropertyValidators::NUMBER_VALIDATOR.getEquivalentNifiStandardValidatorName()) {
+    return MINIFI_VALIDATOR_NUMBER;
+  }
   gsl_FailFast();
 }
 
diff --git a/extension-framework/include/utils/ProcessorConfigUtils.h b/extension-framework/include/utils/ProcessorConfigUtils.h
index b69ed40..b3b5071 100644
--- a/extension-framework/include/utils/ProcessorConfigUtils.h
+++ b/extension-framework/include/utils/ProcessorConfigUtils.h
@@ -55,6 +55,18 @@
       | orThrow(fmt::format("Expected parsable int64_t from \"{}\"", property.name));
 }
 
+inline double parseF64Property(const core::ProcessContext& ctx, const core::PropertyReference& property, const core::FlowFile* flow_file = nullptr) {
+  return ctx.getProperty(property.name, flow_file)
+  | andThen(parsing::parseFloatingPoint<double>)
+  | orThrow(fmt::format("Expected parsable double from \"{}\"", property.name));
+}
+
+inline float parseF32Property(const core::ProcessContext& ctx, const core::PropertyReference& property, const core::FlowFile* flow_file = nullptr) {
+  return ctx.getProperty(property.name, flow_file)
+  | andThen(parsing::parseFloatingPoint<float>)
+  | orThrow(fmt::format("Expected parsable float from \"{}\"", property.name));
+}
+
 inline std::chrono::milliseconds parseDurationProperty(const core::ProcessContext& ctx, const core::PropertyReference& property, const core::FlowFile* flow_file = nullptr) {
   return ctx.getProperty(property.name, flow_file)
       | andThen(parsing::parseDuration<std::chrono::milliseconds>)
@@ -133,7 +145,7 @@
     if (property_str->empty()) {
       return std::nullopt;
     }
-    return parsing::parseFloat(*property_str)
+    return parsing::parseFloatingPoint<float>(*property_str)
         | utils::orThrow(fmt::format("Expected parsable float from \"{}\"", property.name));
   }
   return std::nullopt;
diff --git a/extensions/llamacpp/processors/RunLlamaCppInference.h b/extensions/llamacpp/processors/RunLlamaCppInference.h
index 16cf7a9..67cd14a 100644
--- a/extensions/llamacpp/processors/RunLlamaCppInference.h
+++ b/extensions/llamacpp/processors/RunLlamaCppInference.h
@@ -83,6 +83,7 @@
       .build();
   EXTENSIONAPI static constexpr auto MinP = core::PropertyDefinitionBuilder<>::createProperty("Min P")
       .withDescription("Sets a minimum base probability threshold for token selection. 0.0 = disabled.")
+      .withValidator(core::StandardPropertyValidators::NUMBER_VALIDATOR)
       .build();
   EXTENSIONAPI static constexpr auto MinKeep = core::PropertyDefinitionBuilder<>::createProperty("Min Keep")
       .withDescription("If greater than 0, force samplers to return N possible tokens at minimum.")
diff --git a/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp b/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
index 3582c21..fb22075 100644
--- a/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
+++ b/extensions/llamacpp/tests/RunLlamaCppInferenceTests.cpp
@@ -233,10 +233,6 @@
     REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::TopP.name, "invalid_value"));
     property_name = processors::RunLlamaCppInference::TopP.name;
   }
-  SECTION("Invalid value for Min P property") {
-    REQUIRE(controller.getProcessor()->setProperty(processors::RunLlamaCppInference::MinP.name, "invalid_value"));
-    property_name = processors::RunLlamaCppInference::MinP.name;
-  }
 
   REQUIRE_THROWS(controller.trigger(minifi::test::InputFlowFileData{.content = "42", .attributes = {}}));
   CHECK(minifi::test::utils::verifyLogLinePresenceInPollTime(1s,
diff --git a/extensions/python/ExecutePythonProcessor.cpp b/extensions/python/ExecutePythonProcessor.cpp
index 184acad..7ef4ae7 100644
--- a/extensions/python/ExecutePythonProcessor.cpp
+++ b/extensions/python/ExecutePythonProcessor.cpp
@@ -138,7 +138,8 @@
   DATA_SIZE = 3,
   TIME_PERIOD = 4,
   NON_BLANK = 5,
-  PORT = 6
+  PORT = 6,
+  NUMBER = 7
 };
 
 const core::PropertyValidator& translateCodeToPropertyValidator(const PropertyValidatorCode& code) {
@@ -156,6 +157,8 @@
       return core::StandardPropertyValidators::NON_BLANK_VALIDATOR;
     case PropertyValidatorCode::PORT:
       return core::StandardPropertyValidators::PORT_VALIDATOR;
+    case PropertyValidatorCode::NUMBER:
+      return core::StandardPropertyValidators::NUMBER_VALIDATOR;
     default:
       throw std::invalid_argument("Unknown PropertyValidatorCode");
   }
diff --git a/extensions/python/pythonprocessors/nifiapi/properties.py b/extensions/python/pythonprocessors/nifiapi/properties.py
index 3cbd4c5..fe0dab0 100644
--- a/extensions/python/pythonprocessors/nifiapi/properties.py
+++ b/extensions/python/pythonprocessors/nifiapi/properties.py
@@ -53,6 +53,9 @@
     def createLongValidator(self, *args) -> int:
         return StandardValidators.LONG_VALIDATOR
 
+    def createNumberValidator(self, *args) -> int:
+        return StandardValidators.NUMBER_VALIDATOR
+
 
 class StandardValidators:
     _standard_validators = ValidatorGenerator()
@@ -87,6 +90,7 @@
     TIME_PERIOD_TYPE = 4
     NON_BLANK_TYPE = 5
     PORT_TYPE = 6
+    NUMBER_TYPE = 7
 
 
 def translateStandardValidatorToMiNiFiPropertype(validators: List[int]) -> int:
@@ -108,6 +112,8 @@
         return MinifiPropertyTypes.NON_BLANK_TYPE
     if validator == StandardValidators.PORT_VALIDATOR:
         return MinifiPropertyTypes.PORT_TYPE
+    if validator == StandardValidators.NUMBER_VALIDATOR:
+        return MinifiPropertyTypes.NUMBER_TYPE
     return None
 
 
diff --git a/libminifi/src/minifi-api.cpp b/libminifi/src/minifi-api.cpp
index 1b48434..b1cc566 100644
--- a/libminifi/src/minifi-api.cpp
+++ b/libminifi/src/minifi-api.cpp
@@ -88,6 +88,7 @@
     case MINIFI_VALIDATOR_UNSIGNED_INTEGER: return gsl::make_not_null(&minifi::core::StandardPropertyValidators::UNSIGNED_INTEGER_VALIDATOR);
     case MINIFI_VALIDATOR_DATA_SIZE: return gsl::make_not_null(&minifi::core::StandardPropertyValidators::DATA_SIZE_VALIDATOR);
     case MINIFI_VALIDATOR_PORT: return gsl::make_not_null(&minifi::core::StandardPropertyValidators::PORT_VALIDATOR);
+    case MINIFI_VALIDATOR_NUMBER: return gsl::make_not_null(&minifi::core::StandardPropertyValidators::NUMBER_VALIDATOR);
   }
   gsl_FailFast();
 }
diff --git a/libminifi/test/unit/PropertyValidationTests.cpp b/libminifi/test/unit/PropertyValidationTests.cpp
index 62770fa..0749551 100644
--- a/libminifi/test/unit/PropertyValidationTests.cpp
+++ b/libminifi/test/unit/PropertyValidationTests.cpp
@@ -224,4 +224,16 @@
   CHECK(component.setProperty(property.getName(), "20").error() == core::PropertyErrorCode::ValidationFailed);
 }
 
+TEST_CASE("Number validator") {
+  static constexpr auto property_definition = PropertyDefinitionBuilder<>::createProperty("prop").withValidator(core::StandardPropertyValidators::NUMBER_VALIDATOR).build();
+  const Property property{property_definition};
+  TestConfigurableComponent component;
+  component.setSupportedProperties(std::array<PropertyReference, 1>{property_definition});
+  CHECK(component.setProperty(property.getName(), "20"));
+  CHECK(component.setProperty(property.getName(), "3.14"));
+  CHECK(component.setProperty(property.getName(), "0.0000000001"));
+  CHECK_FALSE(component.setProperty(property.getName(), "10 000"));
+  CHECK_FALSE(component.setProperty(property.getName(), "20 apples"));
+}
+
 }  // namespace org::apache::nifi::minifi::core
diff --git a/minifi-api/common/include/minifi-cpp/core/PropertyValidator.h b/minifi-api/common/include/minifi-cpp/core/PropertyValidator.h
index 2d91905..06f66e9 100644
--- a/minifi-api/common/include/minifi-cpp/core/PropertyValidator.h
+++ b/minifi-api/common/include/minifi-cpp/core/PropertyValidator.h
@@ -25,27 +25,31 @@
 class PropertyValidator {
  public:
   virtual constexpr ~PropertyValidator() {}  // NOLINT can't use = default because of gcc bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93413
+  PropertyValidator() = default;
+  PropertyValidator(const PropertyValidator&) = delete;
+  PropertyValidator(PropertyValidator&&) = delete;
+  PropertyValidator& operator=(const PropertyValidator& other) = delete;
+  PropertyValidator& operator=(PropertyValidator&& other) = delete;
 
   [[nodiscard]] virtual std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const = 0;
   [[nodiscard]] virtual bool validate(std::string_view input) const = 0;
 };
 
-
 class AlwaysValidValidator final : public PropertyValidator {
  public:
-  AlwaysValidValidator() = default;
-  constexpr ~AlwaysValidValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "VALID"; }
-  [[nodiscard]] bool validate(std::string_view) const override { return true; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "VALID";
+  }
+  [[nodiscard]] bool validate(std::string_view) const override {
+    return true;
+  }
 };
 
 class NonBlankValidator final : public PropertyValidator {
  public:
-  NonBlankValidator() = default;
-  constexpr ~NonBlankValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "NON_BLANK_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "NON_BLANK_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     return !utils::string::trim(input).empty();
   }
@@ -53,10 +57,9 @@
 
 class TimePeriodValidator final : public PropertyValidator {
  public:
-  TimePeriodValidator() = default;
-  constexpr ~TimePeriodValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "TIME_PERIOD_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "TIME_PERIOD_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_time = parsing::parseDuration<std::chrono::nanoseconds>(input);
     return parsed_time.has_value();
@@ -65,10 +68,9 @@
 
 class BooleanValidator final : public PropertyValidator {
  public:
-  BooleanValidator() = default;
-  constexpr ~BooleanValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "BOOLEAN_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "BOOLEAN_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_bool = parsing::parseBool(input);
     return parsed_bool.has_value();
@@ -77,10 +79,9 @@
 
 class IntegerValidator final : public PropertyValidator {
  public:
-  IntegerValidator() = default;
-  constexpr ~IntegerValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "INTEGER_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "INTEGER_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_integer = parsing::parseIntegral<int64_t>(input);
     return parsed_integer.has_value();
@@ -89,10 +90,9 @@
 
 class UnsignedIntegerValidator final : public PropertyValidator {
  public:
-  UnsignedIntegerValidator() = default;
-  constexpr ~UnsignedIntegerValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "NON_NEGATIVE_INTEGER_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "NON_NEGATIVE_INTEGER_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_integer = parsing::parseIntegral<uint64_t>(input);
     return parsed_integer.has_value();
@@ -101,10 +101,9 @@
 
 class DataSizeValidator final : public PropertyValidator {
  public:
-  DataSizeValidator() = default;
-  constexpr ~DataSizeValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "DATA_SIZE_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "DATA_SIZE_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_data_size = parsing::parseDataSize(input);
     return parsed_data_size.has_value();
@@ -113,16 +112,26 @@
 
 class PortValidator final : public core::PropertyValidator {
  public:
-  PortValidator() = default;
-  constexpr ~PortValidator() override {}  // NOLINT see comment at parent
-
-  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override { return "PORT_VALIDATOR"; }
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "PORT_VALIDATOR";
+  }
   [[nodiscard]] bool validate(const std::string_view input) const override {
     const auto parsed_integer = parsing::parseIntegralMinMax<uint64_t>(input, 0, 65535);
     return parsed_integer.has_value();
   }
 };
 
+class NumberValidator final : public core::PropertyValidator {
+ public:
+  [[nodiscard]] std::optional<std::string_view> getEquivalentNifiStandardValidatorName() const override {
+    return "NUMBER_VALIDATOR";
+  }
+  [[nodiscard]] bool validate(const std::string_view input) const override {
+    const auto parsed_number = parsing::parseFloatingPoint<double>(input);
+    return parsed_number.has_value();
+  }
+};
+
 namespace StandardPropertyValidators {
 inline constexpr auto ALWAYS_VALID_VALIDATOR = AlwaysValidValidator{};
 inline constexpr auto NON_BLANK_VALIDATOR = NonBlankValidator{};
@@ -132,6 +141,7 @@
 inline constexpr auto UNSIGNED_INTEGER_VALIDATOR = UnsignedIntegerValidator{};
 inline constexpr auto DATA_SIZE_VALIDATOR = DataSizeValidator{};
 inline constexpr auto PORT_VALIDATOR = PortValidator{};
-}
+inline constexpr auto NUMBER_VALIDATOR = NumberValidator{};
+}  // namespace StandardPropertyValidators
 
 }  // namespace org::apache::nifi::minifi::core
diff --git a/minifi-api/include/minifi-api.h b/minifi-api/include/minifi-api.h
index 2ee6f87..6f4cf5b 100644
--- a/minifi-api/include/minifi-api.h
+++ b/minifi-api/include/minifi-api.h
@@ -119,6 +119,7 @@
   MINIFI_VALIDATOR_UNSIGNED_INTEGER = 5,
   MINIFI_VALIDATOR_DATA_SIZE = 6,
   MINIFI_VALIDATOR_PORT = 7,
+  MINIFI_VALIDATOR_NUMBER = 8,
 };
 
 struct minifi_property_definition {