Describe requirements in a declarative, easy-readable format.
When it comes to definition of how an app should work, there are many requirements that should be implemented in source code. Every requirement, obviously, can be described in a human-friendly language, as well as formalized in a programming languge (computer-friendly).
Usually requirements are being implemented in a batch as part of a task/model/etc. without an obvious direct translation of specific requirement into exact line/range of code in the app.
In most cases, every single requirement from specification (task definition) is being translated into some code in data model or business logic and that's it. That means there is not much semantics provided by such implementation - if this requirement is not fullfilled, it's not clear how to report the issue formally to the outer scope and/or in human-friendly format to the user (via GUI). If such reporting is implemented - it usually leads to spreading of the requirement implementation into few different parts: actual requirement check, error description for reporting to outer scope (or API user), and human-friendly representation for reporting via GUI to user.
Such implementation of requirements is hard to test/validate, keep consistent over time (when minor changes happen in a given requirement) and makes source code hard to understand and reason about.
Ideally there should be a tool that allows:
- bind requirement human-friendly description (variable length text) and its computer-friendly formal representation (piece of code) together in a single statement;
- keep focus on content, make the wrapping expressions as minimal as possible;
- automate requirement validation and success/failure reporting to both outer scope and GUI.
Each requirement can be evaluated against a given data value (which can be an atomic or complex data type). In the other words, every requirement definition can be represented in form of a function that takes one or several input parameters and returns Boolean
value - true
means that requirement is fullfilled with provided input values, and false
means the opposite.
The recommended way is to install using SwiftPM, but Carthage is also supported out of the box.
It's a small and very simple, yet powerful library.
Requirement
is the main data type that actually represents a single requirement. Note, that this is a struct
, so once it's created, it works as a single atomic value.
To define a requirement, create an instace of Requirement
. Its consturctor accepts two necessary parameters - human friendly description in form of a String
and a closure that implements formal representation. Moreover, Requirement
is a generic type, Input
generic type represents the type of expected input parameters for the closure.
Here is an example of how to create a requirements, that an integer number should not be equal to zero.
let r = Requirement<Int>("Non-zero") { $0 != 0 }
Same can be achived by using a helper typealias Require
:
let r = Require<Int>("Non-zero") { $0 != 0 }
In the example above we created an instace of Requirement
that supposed to evaluate values of type Int
. We pass a string as the only parameter of constructor function, while second parameter (the closure) is being passed as trailing closure. The closure contains the code thart will be called each time this requirement needs to be evaluated, with corresponding value that needs to be checked as the only input parameter.
Note, that If a requirement contains phrases like AND, OR or any other logical operators, then such requirement should be divided into independent requirements.
When requirement is created, here is an example of how it might be used for checking potentially suitable values.
if
r.isFulfilled(with: 14) // returns Bool
{
// given value - 14 (Int) - fulfills the requirement
// r.title - the description that has been provided
// during requirement initialization
print("\(r.title) -> YES")
}
else
{
// this code block will be executed,
// if 0 will be passed into the r.isFulfilled(...)
print("\(r.title) -> NO")
}
Same check can be done by utilizing Swift error handling, see example below.
do
{
try r.check(with: 0) // this will throw exception
}
catch
{
print(error) // error is of 'RequirementNotFulfilled' type
}
The RequirementNotFulfilled
data type has two parameters:
let requirement: String
that contains description of the requirement;let input: Any
that contains exact input data value that has been evaluated and failed to fulfill the requirement.
While Requirement
itself might be more useful to implement data model, there are several helpers, that use the same idea, but provide special API that is more convenient for inline use when implementing business logic. These helpers are incapsulated into special enum
called REQ
, they all throw an instace of VerificationFailed
error when requirement is not fulfilled, some of them may return a value that can be used further in the code.
When you have an Optional
value or you have a function/closure that produces Optional
value, and you need this value only if it's NOT nil
, or throw an error otherwise:
// the following expression will throw
// if the value from closure is 'nil' or just return
// unwrapped value of the optional from closure overwise
let nonNilValue = try REQ.value("Value is NOT nil") {
// return here an optional value,
// it might be result of an expression
// or an optional value captured from the outer scope
}
Same as the above, but does not return anything. When you have an Optional
value or you have a function/closure that produces Optional
value, and you need to make sure that this value is NOT nil
, or throw an error otherwise:
// the following expression does not return anything,
// it will throw if value IS 'nil'
// or pass through silently otherwise
try REQ.isNotNil("Value is NOT nil") {
// return here an optional value,
// it might be result of an expression
// or an optional value captured from the outer scope
}
When you have an Optional
value or you have a function/closure that produces Optional
value, and you need to make sure that this value IS nil
, or throw an error otherwise:
// the following expression does not return anything,
// it will throw if value is NOT 'nil'
// or pass through silently otherwise
try REQ.isNil("Value IS nil") {
// return here an optional value,
// it might be result of an expression
// or an optional value captured from the outer scope
}
When you have an Bool
value or you have a function/closure that produces Bool
value, and you want to continue only if it's true
, or throw an error otherwise (if it's false
):
// the following expression does not return anything,
// it will throw if value is 'false'
// or pass through silently otherwise
try REQ.isTrue("Value is TRUE") {
// return here a boolean value,
// it might be result of an expression
// or an boolean value captured from the outer scope
}
The VerificationFailed
error type has the only parameter:
let description: String
that contains the requirement description passed to the correspondingREQ.*
function.