Xeme
Xeme provides a common format for returning the results of a process. First we'll look at the Xeme format, which is language independent, then we'll look at how the Xeme gem implements the format.
Xeme structure
The Xeme structure can be used by any software, Ruby or otherwise, as a standard way to report results. A Xeme structure can be stored in any format that recognizes hashes, arrays, strings, numbers, booleans, and null. Such formats include JSON and YAML. In these examples we'll use JSON.
A xeme can be as simple as an empty hash:
{}
That structure indicates no errors, and, in fact, no details at all. However, it also does not explicitly indicate success. In the absence of a defined value of true or false, the result should be considered either undetermined or failed.
To indicate a successful operation, a xeme must have an explicit success
element:
{"success":true}
A xeme can be marked as explicitly failed:
{"success":false}
Any truthy value for success
is considered to indicate success. So, for
example, the following xeme should be considered to indicate success.
{"success":{}}
A xeme may contain other arbitrary information. This is useful if you need more information than just a success or failure.
{"success":true, "tags":["a", "b"]}
Metainformation
A xeme may contain a meta
element. The meta
element can contain a timestamp,
UUID, description, or other meta information.
{
"success":true,
"meta": {
"timestamp": "2023-06-21T08:57:56+00:00",
"uuid": "e11b668c-0823-4b70-aa28-5ac83757a37c",
"description": "directory tests"
}
}
meta
can contain any arbitrary information you want, but several elements, if
present, should follow some standards.
key | description |
---|---|
id | A string, typically without spaces. |
description | A short description of the xeme as a string. |
timestamp | Timestamp in a standard ISO 8601 format. |
UUID | A UUID. |
Nested xemes
A xeme represents the results of a single process. However, a xeme can also contain nested xemes which provide information about sub-processes. In this sense, a xeme can be considered as the final results of the xemes nested within it.
Child xemes are contained in the nested
array.
{
"success":true,
"meta": {"id":"directory"},
"nested":[
{"success":true, "meta": {"id":"database-connection"}},
{"success":true, "meta": {"id":"update"}}
]
}
Nested xemes can themselves have nested xemes, forming a tree of process results.
{
"success":true,
"meta": {"id":"database"},
"nested": [
{
"success":true,
"meta": {"id":"database-connection"},
"nested": [
{"success":true, "meta":{"id":"initialization"}},
{"success":true, "meta":{"id":"disconnection"}}
]
}
]
}
Resolution
Xeme operates on the concept of "least successful outcome". A xeme cannot represent more success than its descendent xemes. So, for example, the following structure contains conflicts:
{
"success":true,
"nested":[
{"success":false}
]
}
It is necessary to resolve these conflicts before the xeme can be considered valid. A conforming processor should implement the following rules:
- If a xeme is marked as failure, all of its ancestor xemes should be marked as failed. So the example above would be resolved as follows:
{
"success":false,
"nested":[
{"success":false}
]
}
A process can be marked as failed regardless of its nested xemes. So there is no conflict in the following structure.
{
"success":false,
"nested":[
{"success":true}
]
}
- If a xeme's
success
is null, then all its ancestors'success
values must be set to null if they are not already set to false. The following structure is invalid.
{
"success":true,
"nested":[
{"success":null}
]
}
It should be resolved as follows:
{
"success":null,
"nested":[
{"success":null}
]
}
Advisory and promise xemes
Three types of Xemes operate on a different ruleset than standard xemes. Those
types are warnings, notes, and promises. They are indicated by the type
property:
{"type": "warning"}
Warnings and notes
Warnings and notes provide advisory information about a process. They have no
affect on the success/failure determination. Warnings provide information about
non-fatal problems in a prosess. Notes do not indicate problems of any kind and
simply provide whatever arbitrary information might be useful. Advisory xemes
should not have success
properties. So, for example, the following structure
is valid, even though the nested advisory xemes do not have success
properties.
{
"success":true,
"nested":[
{"type":"warning", "id":"invalid-setting"},
{"type":"note", "id":"database-connected"}
]
}
Advisory xemes can have nested xemes. However, all descendents of an advisory xeme should themselves be advisory.
Promises
A promise xeme indicates that the success/failure of a process has yet to be
determined. A promise should have a success
value of null, unless it has also
has a truthy value in supplanted
. For example, the following xemes are valid:
{ "type":"promise" }
{ "type":"promise", "supplanted":true, "success": true }
{ "type":"promise", "supplanted":"2023-06-22T06:28:43+0000", "success": true }
Typically, a promise should have an indication of how the promise can be resolved. For example, the following xeme is a promise, with further information to indicate a URI where the final result can be determined, and how soon to query that URI.
{
"type":"promise",
"uri": "https://example.com/20435t",
"delay": 6000
}
No standards are defined on how promises should provide such information. A substandard may be defined down the road.
Best practice is that when a promise xeme is supplanted, it should have a nested xeme that provides the final success/failure of the process.
{
"success": true,
"supplanted": true,
"type":"promise",
"uri": "https://example.com/20435t",
"delay": 6000,
"nested": [
{"success":true}
]
}
Xeme gem
Install
The usual:
sudo gem install xeme
Basic Xeme concepts
Xeme (the gem) is a thin layer over a hash that implements the Xeme format. (For the rest of this document "Xeme" refers to the Ruby class, not the format.) Create a new xeme by instantiating the Xeme class. Instantiation has no required parameters.
require 'xeme'
xeme = Xeme.new
puts xeme # => #<Xeme:0x000055586f1340a8>
If you want to access the hash stored in the xeme object, you can use the object as if it were a hash.
xeme['errors'] = []
xeme['errors'].push({'id'=>'my-error'})
Success and failure
Because a xeme isn't considered successful until it has been explicitly declared so, a new xeme is considered to indicate failure or lack of determination of success/failure.
xeme = Xeme.new
puts xeme.success?.class # => NilClas
To set a xeme to indicate failure, use the fail
method.
xeme = Xeme.new
xeme.fail
puts xeme.success? # => false
Perhaps counter-intuitively, there is no succeed
method. That's because a xeme
cannot be reliably marked as succeeding without checking nested xemes (see
resolution).
Instead there is a try_succeed
method. As its name implies, that method
resolves the xeme and its descendents, only marking the xeme as successful if
resolution allows. We'll get more into handling nested xemes later. The
following example shows setting a single xeme to success using try_succeed
.
xeme = Xeme.new
xeme.try_succeed
puts xeme.success? # => true
try_succeed
will only set a xeme to success if the current success is null or
true. It will not override an explicit setting of false.
xeme = Xeme.new
xeme.fail
xeme.try_succeed
puts xeme.success? # => false
Nesting xemes
Use nest
to create nested xemes. If a block is sent, nest
yields the new
xeme.
xeme.nest() do |child|
# do stuff with nested xeme
end
nest
also returns the new xeme.
child = xeme.nest()
Several methods exist to provide shortcuts for creating nested xemes of various
types: success
, error
, warning
, note
, and promise
.
xeme.success() do |child|
puts child.success? # => true
end
xeme.error() do |child|
puts child.success? # => false
end
# Xeme#failure does same thing as Xeme#error
xeme.failure() do |child|
puts child.success? # => false
end
xeme.warning() do |child|
puts child.class # => Xeme::Warning
end
xeme.note() do |child|
puts child.class # => Xeme::Note
end
xeme.promise() do |child|
puts child.class # => Xeme::Promise
end
Querying nested xemes
Xeme provides several methods for traversing through a stack of nested xemes. In the following examples, we'll use this structure:
xeme = Xeme.new()
xeme.id = 'top'
xeme.nest() do |child|
child.id = 'foo'
child.nest() do |grandchild|
grandchild.id = 'bar'
grandchild.warning.id = 'my-warning'
grandchild.promise.id = 'my-promise'
grandchild.success
end
child.error.id = 'my-error'
child.note.id = 'my-note'
end
The simplest is the all
method, which returns the xeme itself and all nested
xemes as a locked array.
xeme.all.each do |x|
puts x.id
end
# => top
# => foo
# => bar
# => my-warning
# => my-promise
# => my-error
# => my-note
There are several methods for selecting xemes based on their success status.
# xemes marked as success=false
puts xeme.errors.length # => 3
# xemes marked as success=true
puts xeme.successes.length # => 1
# xemes marked as success=null
# does not return advisory xemes
puts xeme.nils.length # => 2
There are also several methods for selecting specific xemes based on their types.
puts xeme.warnings.length # => 1
puts xeme.notes.length # => 2
puts xeme.promises.length # => 1
resolve() and try_succeed()
Xemes can be resolved using the resolve
method.
xeme.resolve
It's common that your process will get to a point, typically at the end of the
script, where you want to mark the process as successful, but only if there were
no errors. Use try_succeed
for that. That method first resolves the xeme and
its descendents, then marks the xeme (and descendents) as successful if there
are no errors.
xeme.try_succeed
puts xeme.success? # => true, false, or nil
The name
The word "xeme" has no particular association with the concept of results reporting. I got the word from a random word generator and I liked it. The xeme, also known as Sabine's gull, is a type of gull. See the Wikipedia page if you'd like to know more.
Author
Mike O'Sullivan mike@idocs.com
History
version | date | notes |
---|---|---|
0.1 | Jan 7, 2020 | Initial upload. |
1.0 | May 29, 2023 | Complete overhaul. Not backward compatible. |
1.1 | May 29, 2023 | Added and cleaned up documentation. No change to functionality. |
1.2 | May 29, 2023 | More cleanup to documentation. |
2.0 | Jun 22, 2023 | Another complete overhaul. This should be the last non-backwards compatible revision. |