|  | 
|  | 1 | +# Constrained Variable | 
|  | 2 | + | 
|  | 3 | + | 
|  | 4 | +[](https://cmake.org) | 
|  | 5 | + | 
|  | 6 | + | 
|  | 7 | + | 
|  | 8 | +## Introduction | 
|  | 9 | + | 
|  | 10 | +A header-only library written in *C++23* for applying composable validation and transformation constraints to variables in a flexible and type-safe way, supporting: | 
|  | 11 | + | 
|  | 12 | +- Range checks. | 
|  | 13 | +- Null and emptiness checks. | 
|  | 14 | +- Enumeration restrictions. | 
|  | 15 | +- Value transformation. | 
|  | 16 | +- Custom predicate validation. | 
|  | 17 | + | 
|  | 18 | +Constraints are applied in a user-defined order and ensure the values are within the expected bounds or rules. | 
|  | 19 | +Helpful error messages are generated when constraints are violated. | 
|  | 20 | + | 
|  | 21 | +## Unit Tests | 
|  | 22 | + | 
|  | 23 | +### Prerequisites | 
|  | 24 | + | 
|  | 25 | +- Install *GoogleTest*. | 
|  | 26 | +- Install *CMake*. | 
|  | 27 | + | 
|  | 28 | +### Building | 
|  | 29 | + | 
|  | 30 | +Go to the project folder and run: | 
|  | 31 | + | 
|  | 32 | +```bash | 
|  | 33 | +mkdir -p build | 
|  | 34 | +cd build | 
|  | 35 | +cmake -DCVAR_BUILD_TESTS=ON .. | 
|  | 36 | +cmake --build . | 
|  | 37 | +``` | 
|  | 38 | + | 
|  | 39 | +### Running | 
|  | 40 | + | 
|  | 41 | +Go to the `build` folder and run: | 
|  | 42 | + | 
|  | 43 | +```bash | 
|  | 44 | +ctest -VV | 
|  | 45 | +``` | 
|  | 46 | + | 
|  | 47 | +## Examples | 
|  | 48 | + | 
|  | 49 | +See more examples in `tests/constrained_var_tests.cpp`. | 
|  | 50 | + | 
|  | 51 | +```c++ | 
|  | 52 | +// Create a constraint. | 
|  | 53 | +constexpr InRange<std::size_t> speed_range {{10, 80}}; | 
|  | 54 | + | 
|  | 55 | +// Create a constrained variable. | 
|  | 56 | +InRangeVariable<std::size_t> speed {speed_range}; | 
|  | 57 | + | 
|  | 58 | +// Try to set a new value. | 
|  | 59 | +speed.Set(input).transform_error([](const Error& err) noexcept { | 
|  | 60 | +    std::println("Failed to set the speed: {}", err.second); | 
|  | 61 | +    return false; | 
|  | 62 | +}); | 
|  | 63 | +``` | 
|  | 64 | +
 | 
|  | 65 | +### Constraints | 
|  | 66 | +
 | 
|  | 67 | +#### `Min` | 
|  | 68 | +
 | 
|  | 69 | +`Min` ensures the value is not less than a specified minimum. | 
|  | 70 | +
 | 
|  | 71 | +```c++ | 
|  | 72 | +constexpr Min<int> min_opt {10}; | 
|  | 73 | +MinVariable<int> var {min_opt}; | 
|  | 74 | +
 | 
|  | 75 | +EXPECT_FALSE(var.Set(5).has_value()); | 
|  | 76 | +EXPECT_EQ(var.Set(15).value_or(0), 15); | 
|  | 77 | +``` | 
|  | 78 | + | 
|  | 79 | +#### `Max` | 
|  | 80 | + | 
|  | 81 | +`Max` ensures the value does not exceed a specified maximum. | 
|  | 82 | + | 
|  | 83 | +```c++ | 
|  | 84 | +constexpr Max<int> max_opt {100}; | 
|  | 85 | +MaxVariable<int> var {max_opt}; | 
|  | 86 | + | 
|  | 87 | +EXPECT_FALSE(var.Set(120).has_value()); | 
|  | 88 | +EXPECT_EQ(var.Set(80).value_or(0), 80); | 
|  | 89 | +``` | 
|  | 90 | +
 | 
|  | 91 | +#### `InRange` | 
|  | 92 | +
 | 
|  | 93 | +`InRange` ensures the value lies within a specified range. | 
|  | 94 | +
 | 
|  | 95 | +```c++ | 
|  | 96 | +constexpr InRange<int> in_range_opt {{10, 100}}; | 
|  | 97 | +InRangeVariable<int> var {in_range_opt}; | 
|  | 98 | +
 | 
|  | 99 | +EXPECT_FALSE(var.Set(5).has_value()); | 
|  | 100 | +EXPECT_EQ(var.Set(50).value_or(0), 50); | 
|  | 101 | +``` | 
|  | 102 | + | 
|  | 103 | +#### `NotInRange` | 
|  | 104 | + | 
|  | 105 | +`NotInRange` ensures the value is not within a specified range. | 
|  | 106 | + | 
|  | 107 | +```c++ | 
|  | 108 | +constexpr NotInRange<int> not_in_range_opt {{10, 100}}; | 
|  | 109 | +NotInRangeVariable<int> var {not_in_range_opt}; | 
|  | 110 | + | 
|  | 111 | +EXPECT_FALSE(var.Set(50).has_value()); | 
|  | 112 | +EXPECT_EQ(var.Set(5).value_or(0), 5); | 
|  | 113 | +``` | 
|  | 114 | +
 | 
|  | 115 | +#### `InSet` | 
|  | 116 | +
 | 
|  | 117 | +`InSet` ensures the value is in a specified set. | 
|  | 118 | +
 | 
|  | 119 | +```c++ | 
|  | 120 | +const InSet<int> in_set_opt {1, 2}; | 
|  | 121 | +InSetVariable<int> var {in_set_opt}; | 
|  | 122 | +
 | 
|  | 123 | +EXPECT_EQ(var.Set(1).value_or(0), 1); | 
|  | 124 | +EXPECT_EQ(var.Set(2).value_or(0), 2); | 
|  | 125 | +EXPECT_FALSE(var.Set(3).has_value()); | 
|  | 126 | +``` | 
|  | 127 | + | 
|  | 128 | +#### `NotInSet` | 
|  | 129 | + | 
|  | 130 | +`NotInSet` ensures the value is not in a specified set. | 
|  | 131 | + | 
|  | 132 | +```c++ | 
|  | 133 | +const NotInSet<int> not_in_set_opt {1, 2}; | 
|  | 134 | +NotInSetVariable<int> var {not_in_set_opt}; | 
|  | 135 | + | 
|  | 136 | +EXPECT_FALSE(var.Set(1).has_value()); | 
|  | 137 | +EXPECT_FALSE(var.Set(2).has_value()); | 
|  | 138 | +EXPECT_EQ(var.Set(3).value_or(0), 3); | 
|  | 139 | +``` | 
|  | 140 | +
 | 
|  | 141 | +#### `Clamp` | 
|  | 142 | +
 | 
|  | 143 | +`Clamp` clamps the value into a specified range (`std::clamp`). | 
|  | 144 | +
 | 
|  | 145 | +```c++ | 
|  | 146 | +constexpr Clamp<int> clamp_opt {{10, 100}}; | 
|  | 147 | +ClampVariable<int> var {clamp_opt}; | 
|  | 148 | +
 | 
|  | 149 | +EXPECT_EQ(var.Set(5).value_or(0), 10); | 
|  | 150 | +EXPECT_EQ(var.Set(120).value_or(0), 100); | 
|  | 151 | +``` | 
|  | 152 | + | 
|  | 153 | +#### `Enum` | 
|  | 154 | + | 
|  | 155 | +`Enum` ensures the enumeration lies within an inclusive range. | 
|  | 156 | + | 
|  | 157 | +```c++ | 
|  | 158 | +enum class Color { White, Red, Green, Black, Invalid }; | 
|  | 159 | + | 
|  | 160 | +template <> | 
|  | 161 | +struct EnumValues<Color> { | 
|  | 162 | +    static constexpr std::array values {Color::White, Color::Red, Color::Green, Color::Black}; | 
|  | 163 | +}; | 
|  | 164 | + | 
|  | 165 | +EnumVariable<Color> var; | 
|  | 166 | + | 
|  | 167 | +EXPECT_FALSE(var.Set(Color::Invalid).has_value()); | 
|  | 168 | +EXPECT_EQ(var.Set(Color::Red).value_or(Color::Invalid), Color::Red); | 
|  | 169 | +``` | 
|  | 170 | +
 | 
|  | 171 | +#### `NotNull` | 
|  | 172 | +
 | 
|  | 173 | +`NotNull` ensures the value is not null or `false`. | 
|  | 174 | +
 | 
|  | 175 | +```c++ | 
|  | 176 | +NotNullVariable<int> var; | 
|  | 177 | +
 | 
|  | 178 | +EXPECT_FALSE(var.Set(0).has_value()); | 
|  | 179 | +EXPECT_EQ(var.Set(42).value_or(0), 42); | 
|  | 180 | +``` | 
|  | 181 | + | 
|  | 182 | +#### `NotEmpty` | 
|  | 183 | + | 
|  | 184 | +`NotEmpty` ensures the container like `std::vector` is not empty. | 
|  | 185 | + | 
|  | 186 | +```c++ | 
|  | 187 | +NotEmptyVariable<std::vector<int>> var; | 
|  | 188 | + | 
|  | 189 | +EXPECT_FALSE(var.Set(std::vector<int> {}).has_value()); | 
|  | 190 | +EXPECT_EQ(var.Set(std::vector<int> {1}).value_or(std::vector<int> {}), std::vector<int> {1}); | 
|  | 191 | +``` | 
|  | 192 | +
 | 
|  | 193 | +#### `Predicate` | 
|  | 194 | +
 | 
|  | 195 | +`Predicate` ensures the value satisfies a predicate. | 
|  | 196 | +
 | 
|  | 197 | +```c++ | 
|  | 198 | +const Predicate<int> pred {[](const int x) noexcept { | 
|  | 199 | +    return x % 2 == 0; | 
|  | 200 | +}}; | 
|  | 201 | +
 | 
|  | 202 | +PredicateVariable<int> var {pred}; | 
|  | 203 | +
 | 
|  | 204 | +EXPECT_FALSE(var.Set(3).has_value()); | 
|  | 205 | +EXPECT_EQ(var.Set(4).value_or(0), 4); | 
|  | 206 | +``` | 
|  | 207 | + | 
|  | 208 | +#### `Transformer` | 
|  | 209 | + | 
|  | 210 | +`Transformer` transforms the value before validation or storage, supporting chaining with other constraints. | 
|  | 211 | + | 
|  | 212 | +```c++ | 
|  | 213 | +const Transformer<int, double> func {[](const int v) noexcept { | 
|  | 214 | +    return static_cast<double>(v); | 
|  | 215 | +}}; | 
|  | 216 | + | 
|  | 217 | +TransformerVariable<int, double> var {func}; | 
|  | 218 | + | 
|  | 219 | +EXPECT_EQ(var.Set(5).value_or(0.0), 5.0); | 
|  | 220 | +``` | 
|  | 221 | +
 | 
|  | 222 | +### Constraint Chains | 
|  | 223 | +
 | 
|  | 224 | +This example transforms an integer to a vector as its size and then check if it is empty using a chain of `Transformer` and `NotEmpty`. | 
|  | 225 | +
 | 
|  | 226 | +```c++ | 
|  | 227 | +ChainType<std::vector<int>> SizeToVector(const std::size_t& size) noexcept { | 
|  | 228 | +    return std::vector<int>(size); | 
|  | 229 | +} | 
|  | 230 | +
 | 
|  | 231 | +constexpr NotEmpty<std::vector<int>> not_empty_opt; | 
|  | 232 | +const Transformer<std::size_t, std::vector<int>> transformer_opt {SizeToVector}; | 
|  | 233 | +ConstrainedVariable<std::vector<int>, decltype(transformer_opt), decltype(not_empty_opt)> var {transformer_opt, not_empty_opt}; | 
|  | 234 | +
 | 
|  | 235 | +EXPECT_FALSE(var.Set(0).has_value()); | 
|  | 236 | +EXPECT_EQ(var.Set(1).value_or(std::vector<int> {}), std::vector<int>(1)); | 
|  | 237 | +``` | 
|  | 238 | + | 
|  | 239 | +This example defines a boolean variable that is `true` only when the input value is `1` or `2` using a chain of `InSet` and `Transformer`. | 
|  | 240 | + | 
|  | 241 | +```c++ | 
|  | 242 | +const InSet<int> in_set_opt {1, 2}; | 
|  | 243 | +const Transformer<ChainType<int>, bool> transformer_opt { | 
|  | 244 | +    [](const ChainType<int>& val) noexcept { | 
|  | 245 | +        return val.has_value(); | 
|  | 246 | +    } | 
|  | 247 | +}; | 
|  | 248 | + | 
|  | 249 | +ConstrainedVariable<bool, decltype(in_set_opt), decltype(transformer_opt)> var {in_set_opt, transformer_opt}; | 
|  | 250 | + | 
|  | 251 | +EXPECT_TRUE(var.Set(1).has_value()); | 
|  | 252 | +EXPECT_TRUE(var.Get()); | 
|  | 253 | + | 
|  | 254 | +EXPECT_TRUE(var.Set(3).has_value()); | 
|  | 255 | +EXPECT_FALSE(var.Get()); | 
|  | 256 | +``` | 
|  | 257 | +
 | 
|  | 258 | +In most cases, the parameter type of `Apply` is exactly the same as the return type of the previous constraint in the chain. | 
|  | 259 | +For example, the return type of `Set<int>::Apply` and the parameter type of `Transformer<int, bool>::Apply` are both `ChainType<int>`. | 
|  | 260 | +In this case, if the previous constraint returns an `std::unexpected`, the user-provided transformation function will not be called. | 
|  | 261 | +
 | 
|  | 262 | +But currently we want the transformation function to return the validity of the previous constraint's return value. | 
|  | 263 | +The constraint chain of `Set<int>` and `Transformer<int, bool>` does not work because the when the number is not in the set, the transformer will be skipped. | 
|  | 264 | +Instead, we should use `Transformer<ChainType<int>, bool>`. | 
|  | 265 | +The parameter type of its `Apply` is `ChainType<ChainType<int>>`. | 
|  | 266 | +Regardless of whether `Set<int>` returns a number or an `std::unexpected`, the result will always be forwarded to the user-provided function. | 
|  | 267 | +
 | 
|  | 268 | +### Validated Boolean Variables | 
|  | 269 | +
 | 
|  | 270 | +You can directly use `ValidatedBoolVariable` if you need a boolean variable validated against a set of constraints. | 
|  | 271 | +
 | 
|  | 272 | +```c++ | 
|  | 273 | +const InSet<int> in_set_opt {1, 2}; | 
|  | 274 | +ValidatedBoolVariable<int, decltype(in_set_opt)> var {in_set_opt}; | 
|  | 275 | +
 | 
|  | 276 | +EXPECT_TRUE(var.Set(1)); | 
|  | 277 | +EXPECT_TRUE(var.Set(2)); | 
|  | 278 | +EXPECT_FALSE(var.Set(3)); | 
|  | 279 | +``` | 
|  | 280 | + | 
|  | 281 | +### Validation | 
|  | 282 | + | 
|  | 283 | +If you only need to validate values without storing them, you can directly `ConstraintChain` and `ValidationChain`. | 
|  | 284 | + | 
|  | 285 | +```c++ | 
|  | 286 | +const InSet<int> in_set_opt {1, 2}; | 
|  | 287 | +ValidationChain<int, decltype(in_set_opt)> var {in_set_opt}; | 
|  | 288 | + | 
|  | 289 | +EXPECT_TRUE(var.Apply(1)); | 
|  | 290 | +EXPECT_TRUE(var.Apply(2)); | 
|  | 291 | +EXPECT_FALSE(var.Apply(3)); | 
|  | 292 | +``` | 
|  | 293 | +
 | 
|  | 294 | +## License | 
|  | 295 | +
 | 
|  | 296 | +Distributed under the *MIT License*. See `LICENSE` for more information. | 
0 commit comments