A Universal Result (UR) is an object that allows the programmer to return either a value or a sequence of messages (or both) from a procedure. This could, of course, be done by hand using tuple or other options, but the goal of this package is two-fold:
- Make it easy (and predictable) to create such "dynamic" results.
- Make it possible to integrate such a system with other libraries.
Table of Contents
The following is a very simple example of UR.
First, we are going to import the library and "wrap" the type of element we want to return.
import ur
type
Vector = tuple[x: float, y: float]
wrap_UR(Vector)
The wrap_UR
macro creates a UR_Vector
object with large set of
useful methods.
(Don't worry, with conditional compiling, Nim should later remove the methods you don't use.)
Now, we use the new object for returning a flexible result:
import ur
type
Vector = tuple[x: float, y: float]
wrap_UR(Vector)
proc reduceXByNumber(v: Vector, denominator: float): UR_Vector =
result = newUR_Vector() # this procedure was generated by 'wrap_UR'
if denominator == 0.0:
result.set_failure("You can't divide by zero; Ever")
return
if denominator < 0.0:
result.set_failure("Negative denominators are not allowed")
return
if denominator < 0.1:
result.set_warning("That is an awefully small denominator")
var newVector = v
newVector.x = newVector.x / denominator
result.value = newVector
result.set_expected_success("Vector x reduced")
Now let's use it:
var a: Vector = (4.0, 3.2)
var response = reduceXByNumber(a, 2.0)
if response.ok:
echo "my new x is " & $response.value.x
should display:
my new x is 2.0
and
response = reduceXByNumber(a, 0.0)
if not response.ok:
echo "error messages: "
echo $response
should display:
error messages: UR events: (class: danger, msg: You can't divide by zero; Ever)
and
response = reduceXByNumber(a, 0.0001)
if response.ok:
echo "my new x is " & $response.value.x
if response.has_warning:
echo "my warnings are " & $response.warning_msgs
should display:
my new x is 40000.0 my warnings are @["That is an awefully small denominator"]
In general, if a returned result is .ok
then there is a .value
. If it is
not .ok
, then there isn't and the details are in the events created.
However, even .ok
events can have success
, info
, and warning
messages.
UR already has one library integrated: Nim's standard
logging
module. You can use it by importing 'ur/log'.
For example:
import
strutils,
logging
import
ur,
ur/log
var L = newFileLogger("test.log", fmtStr = verboseFmtStr)
addHandler(L)
type
Vector = tuple[x: float, y: float]
wrap_UR(Vector)
proc example(v: Vector): UR_Vector:
result = newUR_Vector()
result.value = v
result.value.x = result.value.x + 1.0
result.set_expected_success("x incremented by 1.0")
var a = Vector(x: 9.3, y: 3.0)
var response = a.example()
echo "message: $1, x: $2".format(response.msg, response.value.x)
response.sendLog() # this sends the event(s) to logging
Now "test.log" will have an entry similar to this:
D, [2018-06-29T12:34:42] -- app: success; user; x incremented by 1.0
All filtering for sendLog
is done by logging
; and that library
strictly looks at the level
attribute.
UR is all about the automatically generate UR_object objects. The objects are defined internally as:
type
URevent*
msg*: string
level*: Level
class*: DisplayClass
audience*: Audience
UR_<type>
events*: seq[URevent]
value*: <type>
So, essentially, there is a list of events (messages) and the value being returned.
Each event has a message and three very distinct attributes.
The level
is the degree of distribution for the message.
It answers the question: How Important is This?
The available levels:
lvlAll
lvlDebug
lvlInfo
lvlNotice
lvlWarn
lvlError
lvlFatal
lvlNone
The level
definitions are set by the logging
standard library
that is part of Nim. See: https://nim-lang.org/docs/logging.html
NOTE: the names of the levels are somewhat misleading. Using a level of
lvlError
does NOT mean that an error has occured. It means "if I'm
filtering a log for mostly errors, this message should show up in that
log".
For judging the character of the event, use the class
.
The class
is the judgement of the event.
it answers the question: Is this a good or bad event?
Only four classes are possible:
info
- a neutral message adding extra informationsuccess
- everything workedwarning
- everything worked, but something is suspiciousdanger
- failure/error/bug
The class
definitions are from the Boostrap CSS project. See:
https://getbootstrap.com
The audience
is, not surpisingly, the intended audience for any
message about the event.
In a traditional 'logging' or SYSLOG system, the intended audience is
strictly ops
. UR allows for further targets; useful when UR is
integrated with web apps or other development frameworks.
It answers the question: Who is permitted to see This?
The possible audiences are:
ops
- IT staff, developers, software agentsadmin
- users with admin clearanceuser
- regular end users / registered memberspublic
- the whole world (no restrictions)
Each audience permission is more restrictive than the previous. So,
ops
can see all events. But admin
can only see admin
,
user
and public
events. And so on.
The attributes are meant to be combined when making decisions.
For example, an event with an audience
of user
but a level
of lvlDebug
probably won't be shown to the end user. Essentially,
they have permission to see the message, but won't because harrasing an
end user with debug messages is not a friendly thing to do.
There is also wrapper called wrap_UR_detail
that adds a table of
strings to a UR called detail
. The purpose of this is to allow more
sophisticated logging and handling of events. Of course, adding such
support also increases the overhead of UR; so please take that into
consideration.
Building on the earlier example for logging:
import
strutils,
logging
import
ur,
ur/log
var L = newFileLogger("test.log", fmtStr = verboseFmtStr)
addHandler(L)
type
Vector = tuple[x: float, y: float]
wrap_UR_detail(Vector)
proc example(v: Vector, category: string): UR_Vector:
result = newUR_Vector()
result.value = v
result.value.x = result.value.x + 1.0
result.set_expected_success("x incremented by 1.0")
result.detail["category"] = category
var a = Vector(x: 9.3, y: 3.0)
var response = a.example("project abc")
echo "message: $1, category: $2".format(response.msg, response.detail["category"])
To use the detail in the context of ur/log
, there is a procedure
called setURLogFormat
. It is expecting a pointer to a procedure.
That procedure must have the following parameters:
(event: UREvent, detail: Table[string, string]): string
So, for example:
var L = newFileLogger("test.log", fmtStr = verboseFmtStr)
addHandler(L)
proc my_example_format(event: UREvent, detail: Table[string, string]): string =
var category = "unknown"
if detail.hasKey("category"):
category = detail["category"]
result = "[$1] [$2] $3".format(event.class, category, event.msg)
setURLogFormat(my_example_format)
Now, the entry in "test.log" will look like:
D, [2018-06-29T12:34:42] -- app: [success] [project abc] x incremented by 1.0
NOTE: the setURLLogFormat
procedure also works with the simpler
wrap_UR
. The detail
table will simply be empty.