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
A Simple Example
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)
wrap_UR macro creates a
UR_Vector object with large set of
(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
my new x is 2.0
response = reduceXByNumber(a, 0.0) if not response.ok: echo "error messages: " echo $response
error messages: UR events: (class: danger, msg: You can't divide by zero; Ever)
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
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
.ok, then there isn't and the details are in the events created.
.ok events can have
Using With Logging
UR already has one library integrated: Nim's standard
logging module. You can use it by importing 'ur/log'.
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
The UR Object
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.
level is the degree of distribution for the message.
It answers the question: How Important is This?
The available levels:
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
For judging the character of the event, use 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 information
success- everything worked
warning- everything worked, but something is suspicious
class definitions are from the Boostrap CSS project. See:
audience is, not surpisingly, the intended audience for any
message about the event.
In a traditional 'logging' or SYSLOG system, the intended audience is
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 agents
admin- users with admin clearance
user- regular end users / registered members
public- 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
public events. And so on.
Combining the attributes together.
The attributes are meant to be combined when making decisions.
For example, an event with an
user but a
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.
Bonus: Adding Detail
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
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
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
setURLLogFormat procedure also works with the simpler
detail table will simply be empty.