/ur

Nim Universal Result (UR) system

Primary LanguageNimMIT LicenseMIT

Module ur

nimble

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:

  1. Make it easy (and predictable) to create such "dynamic" results.
  2. Make it possible to integrate such a system with other libraries.

Table of Contents

Video Introduction

Using UR: A Nim Library

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)

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.

Using With Logging

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.

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

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.

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 information
  • success - everything worked
  • warning - everything worked, but something is suspicious
  • danger - failure/error/bug

The class definitions are from the Boostrap CSS project. See: https://getbootstrap.com

audience

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 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 admin, user and 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 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.

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 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.

See also