xeme 0.3 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +352 -260
- data/lib/xeme.rb +423 -414
- metadata +24 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd9c9b62b1d0d2f4db4657ff6880cea68476a47ca4e366da77eab004d5ad71f
|
4
|
+
data.tar.gz: b15afd00d574e1382597d43df90f50668e0c8a651f2623ab5447af0d717bd404
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 800b710d55a2745c7591a8c5867c5b97125750d525b9ed941f8826093c60efac36f03a135ca9104728ffe849351d176f257a0dc13382a25a87b5d2e9f553653f
|
7
|
+
data.tar.gz: 90c33ffa2e86e06895176ce1a16aa06fef19991fb0c7fbcadc8cb79f9480d6fc4ee1764e162d8b5e3e00993c07523926d38befb74c1a3b9ef22193b968eb64ff
|
data/README.md
CHANGED
@@ -1,263 +1,362 @@
|
|
1
1
|
# Xeme
|
2
2
|
|
3
|
-
Xeme provides a common format for returning the results of a process.
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
xeme = Xeme.new
|
8
|
-
xeme.error 'error-1'
|
9
|
-
|
10
|
-
If there are any errors, such as in this first example, <tt>success</tt> returns false:
|
11
|
-
|
12
|
-
puts xeme.success # false
|
13
|
-
|
14
|
-
A Xeme object without any errors returns true for <tt>success</tt>:
|
15
|
-
|
16
|
-
xeme = Xeme.new
|
17
|
-
puts xeme.success # true
|
18
|
-
|
19
|
-
Xeme is a good way to report results that are more complex than just success or
|
20
|
-
failure. For example, consider the situation in which a record must have a name
|
21
|
-
field and an email field. If the record does not meet these requirements, it is
|
22
|
-
not sufficient to merely report success or failure, nor to just report that one
|
23
|
-
field or the other is missing. With Xeme you can report all of that information
|
24
|
-
in a single object:
|
25
|
-
|
26
|
-
xeme = Xeme.new
|
27
|
-
xeme.error 'name'
|
28
|
-
xeme.error 'email'
|
29
|
-
|
30
|
-
You can return the Xeme object to another Ruby process, or you can output it to
|
31
|
-
JSON for use in some other program:
|
32
|
-
|
33
|
-
puts xeme.to_json
|
34
|
-
|
35
|
-
# outputs:
|
36
|
-
{"success":false,"messages":{"errors":[{"id":"name"},{"id":"email"}]}}
|
37
|
-
|
38
|
-
You might prefer to add the <tt>pretty</tt> option for more readable output:
|
39
|
-
|
40
|
-
puts xeme.to_json('pretty'=>true)
|
41
|
-
|
42
|
-
# outputs:
|
43
|
-
{
|
44
|
-
"success": false,
|
45
|
-
"messages": {
|
46
|
-
"errors": [
|
47
|
-
{
|
48
|
-
"id": "name"
|
49
|
-
},
|
50
|
-
{
|
51
|
-
"id": "email"
|
52
|
-
}
|
53
|
-
]
|
54
|
-
}
|
55
|
-
}
|
56
|
-
|
57
|
-
In some situations, it's useful to give details about the error. For example,
|
58
|
-
if the <tt>name</tt> field is too long, you can report not just that there is
|
59
|
-
an error in that field, but specifics about that error:
|
60
|
-
|
61
|
-
xeme.error('name') do |msg|
|
62
|
-
msg['too-long'] = true
|
63
|
-
msg['max-length'] = 45
|
64
|
-
end
|
65
|
-
|
66
|
-
That would produce a structure like this:
|
67
|
-
|
68
|
-
{
|
69
|
-
"success": false,
|
70
|
-
"messages": {
|
71
|
-
"errors": [
|
72
|
-
{
|
73
|
-
"id": "name",
|
74
|
-
"details": {
|
75
|
-
"too-long": true,
|
76
|
-
"max-length": 45
|
77
|
-
}
|
78
|
-
}
|
79
|
-
]
|
80
|
-
}
|
81
|
-
}
|
82
|
-
|
83
|
-
|
84
|
-
You can also add warnings and notes. A warning indicates a problem but not an
|
85
|
-
actual failure. A note does not indicate any problem at all but just some
|
86
|
-
information that should be passed along.
|
87
|
-
|
88
|
-
xeme.warning 'database-reset'
|
89
|
-
xeme.note 'database ok'
|
90
|
-
|
91
|
-
It's often useful to add misc details to the results. For example, a database
|
92
|
-
query might result in some rows from a table. You might add these rows to the
|
93
|
-
<tt>misc</tt> property like this:
|
94
|
-
|
95
|
-
xeme = Xeme.new
|
96
|
-
xeme.misc['rows'] = []
|
97
|
-
xeme.misc['rows'].push 'a'
|
98
|
-
xeme.misc['rows'].push 'b'
|
99
|
-
xeme.misc['rows'].push 'c'
|
100
|
-
puts xeme.to_json('pretty'=>true)
|
101
|
-
|
102
|
-
# outputs:
|
103
|
-
{
|
104
|
-
"success": true,
|
105
|
-
"misc": {
|
106
|
-
"rows": [
|
107
|
-
"a",
|
108
|
-
"b",
|
109
|
-
"c"
|
110
|
-
]
|
111
|
-
}
|
112
|
-
}
|
113
|
-
|
114
|
-
Xeme can input a Xeme JSON structure to make a new Xeme object:
|
115
|
-
|
116
|
-
# json
|
117
|
-
json = <<~'JSON'
|
118
|
-
{
|
119
|
-
"success": false,
|
120
|
-
"messages": {
|
121
|
-
"errors": [
|
122
|
-
{ "id": "location" }
|
123
|
-
]
|
124
|
-
}
|
125
|
-
}
|
126
|
-
JSON
|
127
|
-
|
128
|
-
# xeme
|
129
|
-
xeme = Xeme.from_json(json)
|
3
|
+
Xeme provides a common format for returning the results of a process. First
|
4
|
+
we'll look at the Xeme format, which is language independent, then we'll look at
|
5
|
+
how the Xeme gem implements the format.
|
6
|
+
|
130
7
|
|
131
8
|
## Xeme structure
|
132
9
|
|
133
10
|
The Xeme structure can be used by any software, Ruby or otherwise, as a standard
|
134
11
|
way to report results. A Xeme structure can be stored in any format that
|
135
12
|
recognizes hashes, arrays, strings, numbers, booleans, and null. Such formats
|
136
|
-
include JSON and YAML.
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
That structure indicates no
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
13
|
+
include JSON and YAML. In these examples we'll use JSON.
|
14
|
+
|
15
|
+
A xeme can be as simple as an empty hash:
|
16
|
+
|
17
|
+
```json
|
18
|
+
{}
|
19
|
+
```
|
20
|
+
|
21
|
+
That structure indicates no errors, and, in fact, no details at all. However, it
|
22
|
+
also does not explicitly indicate success, so that xeme would be considered to
|
23
|
+
have failed.
|
24
|
+
|
25
|
+
To indicate a successful operation, a xeme must have an explicit `success`
|
26
|
+
element:
|
27
|
+
|
28
|
+
```json
|
29
|
+
{"success":true}
|
30
|
+
```
|
31
|
+
|
32
|
+
A xeme can be marked as explicitly failed:
|
33
|
+
|
34
|
+
```json
|
35
|
+
{"success":false}
|
36
|
+
```
|
37
|
+
|
38
|
+
If a xeme does not have an explicit `success` element, or it is set to `nil`, it
|
39
|
+
should be considered to have failed. However, depending on how you process the
|
40
|
+
xeme, `nil` could be considered as having not finished the test. In that case,
|
41
|
+
consider using [promises](#promises).
|
42
|
+
|
43
|
+
### Messages
|
44
|
+
|
45
|
+
A message is an error, a warning, a note, or a promise. Each type of message has
|
46
|
+
an array. For example, this xeme has two errors:
|
47
|
+
|
48
|
+
```json
|
49
|
+
{ "errors":[{"id":"http-fault"}, {"id":"transaction-error"}] }
|
50
|
+
```
|
51
|
+
|
52
|
+
A message does not have to have any particular structure, but best practice is
|
53
|
+
to give each message an identifier (`id`).
|
54
|
+
|
55
|
+
If there are any errors then that indicates a failure, regardless of the value
|
56
|
+
of `success`. Any implementation of Xeme should have a method for resolving
|
57
|
+
conflicts between `success` and `errors`, always giving priority to the presence
|
58
|
+
of errors over the value of `success`.
|
59
|
+
|
60
|
+
If a xeme has any promises then that indicates not success, though not
|
61
|
+
necessarily explicit failure. If a xeme is marked as successful, but there are
|
62
|
+
promises, then `success` should be considered nil.
|
63
|
+
|
64
|
+
Warnings indicate problems that don't outright cause a failure. Notes are
|
65
|
+
messages that don't indicate a problem of any kind.
|
66
|
+
|
67
|
+
### Metainformation
|
68
|
+
|
69
|
+
Sometimes it's useful to store metainformation the xeme. For example, log files
|
70
|
+
are more discoverable if each xeme has a unique id and timestamp. Generally a
|
71
|
+
xeme will have at least to elements, `uuid` and `timestamp`:
|
72
|
+
|
73
|
+
```json
|
74
|
+
{
|
75
|
+
"meta":{
|
76
|
+
"uuid":"dae1cf26-e8fa-43fa-bedc-88fea10255f4",
|
77
|
+
"timestamp":"2023-05-25 03:46:19 -0400"
|
78
|
+
}
|
79
|
+
}
|
80
|
+
```
|
81
|
+
|
82
|
+
### Nested xemes
|
83
|
+
|
84
|
+
A xeme can have nested xemes within it. Those nested xemes go in the `nested`
|
85
|
+
array:
|
86
|
+
|
87
|
+
```json
|
88
|
+
{
|
89
|
+
"nested": [
|
90
|
+
{ "success": true },
|
91
|
+
{ "errors": [{"id":"server-fault"}] },
|
92
|
+
]
|
93
|
+
}
|
94
|
+
```
|
95
|
+
|
96
|
+
For a xeme to be considered successful, all of its nested xemes must be marked
|
97
|
+
as success. If any nested xemes have errors, then the outermost xeme is
|
98
|
+
considered to be considered as failed. If any nested xemes has `success` as nil,
|
99
|
+
then the outermost xeme cannot `success` as true, though it can be explicitly
|
100
|
+
false.
|
101
|
+
|
102
|
+
## Xeme gem
|
103
|
+
|
104
|
+
### Install
|
105
|
+
|
106
|
+
The usual:
|
107
|
+
|
108
|
+
```sh
|
109
|
+
gem install xeme
|
110
|
+
```
|
111
|
+
|
112
|
+
### Using the Xeme gem
|
113
|
+
|
114
|
+
Xeme (the gem) is a thin layer over a hash that implements the Xeme format. (For
|
115
|
+
the rest of this document "Xeme" refers to the Ruby class, not the format.)
|
116
|
+
Create a new xeme by instantiating the Xeme class. Instantiation has no required
|
117
|
+
parameters.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
require 'xeme'
|
121
|
+
xeme = Xeme.new
|
122
|
+
puts xeme # => #<Xeme:0x000055586f1340a8>
|
123
|
+
```
|
124
|
+
|
125
|
+
Sometimes it's handy to give a xeme an identifier. You can do that by passing in
|
126
|
+
a string in `Xeme.new`.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
xeme = Xeme.new('my-xeme')
|
130
|
+
puts xeme.id # => my-xeme
|
131
|
+
```
|
132
|
+
|
133
|
+
If you want to access the hash stored in the xeme object, you can use the object
|
134
|
+
as if it were a hash.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
xeme['errors'] = []
|
138
|
+
xeme['errors'].push({'id'=>'my-error'})
|
139
|
+
```
|
140
|
+
|
141
|
+
#### Success and failure
|
142
|
+
|
143
|
+
Because a xeme isn't considered successful until it has been explicitly declared
|
144
|
+
so, a new xeme is considered to indicate failure. However, because there are no
|
145
|
+
errors and `success` has not been explicitly set, `success?` returns `nil`
|
146
|
+
instead of `false`.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
xeme = Xeme.new
|
150
|
+
puts xeme.success?.class # => NilClas
|
151
|
+
```
|
152
|
+
|
153
|
+
There are two ways to mark a xeme as successful, one of which usually the better
|
154
|
+
choice. The not-so-good way to mark success is with the `succeed` method.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
xeme = Xeme.new
|
158
|
+
xeme.succeed
|
159
|
+
puts xeme.success? # => true
|
160
|
+
```
|
161
|
+
|
162
|
+
The problem with `succeed` is that if there are errors, `succeed` will raise an
|
163
|
+
exception.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
xeme = Xeme.new
|
167
|
+
xeme.error 'my-error'
|
168
|
+
xeme.succeed # => raises exception: `succeed': cannot-set-to-success: errors
|
169
|
+
```
|
170
|
+
|
171
|
+
A better option is `try_succeed`. If your script gets to a point, usually at the
|
172
|
+
end of the function or script, that you want to set the xeme to success, but
|
173
|
+
only if there are no errors, use `try_succeed`.
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
xeme = Xeme.new
|
177
|
+
xeme.try_succeed
|
178
|
+
puts xeme.success? # => true
|
179
|
+
```
|
180
|
+
|
181
|
+
If there are errors, `try_succeed` won't raise an exception, but will not set
|
182
|
+
the xeme to failure.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
xeme = Xeme.new
|
186
|
+
xeme.error 'my-error'
|
187
|
+
xeme.try_succeed
|
188
|
+
puts xeme.success? # => false
|
189
|
+
```
|
190
|
+
|
191
|
+
#### Creating and using messages
|
192
|
+
|
193
|
+
Messages in a xeme provide a way to indicate errors (i.e. fatal errors),
|
194
|
+
warnings (non-fatal errors), notes (not an error at all), and promises (guides
|
195
|
+
to getting the final success or failure of the process). A message is a hash
|
196
|
+
with whatever arbitrary information you want to add. Each type of message has
|
197
|
+
its own method for creating it, an array for storing them, and methods for
|
198
|
+
checking if any exist. The following script creates an error, a warning, a
|
199
|
+
note, and a promise.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
xeme = Xeme.new
|
203
|
+
xeme.error 'my-error'
|
204
|
+
xeme.warning 'my-warning'
|
205
|
+
xeme.note 'my-note'
|
206
|
+
xeme.promise 'my-promise'
|
207
|
+
```
|
208
|
+
|
209
|
+
`error`, `warning`, and `note` each create a message for their own type.
|
210
|
+
Although it is not required, it's usually a good idea to give a string as the
|
211
|
+
first parameter. That string will be set to the `id` element in the resulting
|
212
|
+
hash, as seen in the example above.
|
213
|
+
|
214
|
+
`errors`, `warnings`, and `notes` return arrays for each type.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
xeme.errors.each do |e|
|
218
|
+
puts e['id'] # => my-error
|
219
|
+
end
|
220
|
+
|
221
|
+
xeme.warnings.each do |w|
|
222
|
+
puts w['id'] # => my-warning
|
223
|
+
end
|
224
|
+
|
225
|
+
xeme.notes.each do |n|
|
226
|
+
puts n['id'] # => my-note
|
227
|
+
end
|
228
|
+
|
229
|
+
xeme.promises.each do |p|
|
230
|
+
puts p['id'] # => my-promise
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
**Gotcha:** These methods return frozen arrays, *not* the arrays in the xeme.
|
235
|
+
This is because these methods return not only the xeme's own message arrays, but
|
236
|
+
also any nested messages. See [Nesting xemes](#nesting-xemes) below.
|
237
|
+
|
238
|
+
There are several ways to create and populate a message. Choose whichever is
|
239
|
+
preferable to you. One way is demonstrated in the example above. You simply call
|
240
|
+
the appropriate method, passing in an identifier. If all you want to do is
|
241
|
+
create a message with an `id` then that's probably the easiest choice.
|
242
|
+
|
243
|
+
Another way to use a `do` block to add custom information to the message.
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
xeme.error('my-error') do |error|
|
247
|
+
error['database-error'] = 'some database error'
|
248
|
+
error['commands'] = ['a', 'b', 'c']
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
Remember a message is just a hash, so you can add any kind of structure to the
|
253
|
+
hash you want such a strings, booleans, hashes, and arrays.
|
254
|
+
|
255
|
+
Finally, the message command returns the new message. So, if you want, you can
|
256
|
+
assign that return value to a variable and work with the message that way.
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
err = xeme.error('my-error')
|
260
|
+
err['database-error'] = 'some database error'
|
261
|
+
err['commands'] = ['a', 'b', 'c']
|
262
|
+
```
|
263
|
+
|
264
|
+
#### Nesting xemes
|
265
|
+
|
266
|
+
In complex testing situations it can be useful to nest results within other
|
267
|
+
results. To nest a xeme within another xeme, use the `#nest` method:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
xeme = Xeme.new('results')
|
271
|
+
xeme.nest 'child-xeme'
|
272
|
+
```
|
273
|
+
|
274
|
+
You probably want to do something more than just create a nested xeme, so you
|
275
|
+
can use a `do` block to work with the nested xeme:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
xeme.nest('child-xeme') do |child|
|
279
|
+
child.error 'child-error'
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
You can loop through all xemes, including the outermost xeme and all nested
|
284
|
+
xemes, using the `#all` method.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
xeme.all.each do |x|
|
288
|
+
puts x.id
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
**Nested messages**
|
293
|
+
|
294
|
+
The `#errors`, `#warnings`, `#notes`, and `#promises` methods return arrays of
|
295
|
+
all messages within the xeme, including the outermost xeme and nested xemes.
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
xeme = Xeme.new
|
299
|
+
xeme.error 'outer-error'
|
300
|
+
|
301
|
+
xeme.nest do |child|
|
302
|
+
child.error 'child-error'
|
303
|
+
end
|
304
|
+
|
305
|
+
puts xeme.errors
|
306
|
+
|
307
|
+
# => {"id"=>"outer-error"}
|
308
|
+
# => {"id"=>"child-error"}
|
309
|
+
```
|
310
|
+
|
311
|
+
If you want to search for messages with a specific `id`, add that id to the
|
312
|
+
messages method:
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
puts xeme.errors('child-error') # => {"id"=>"child-error"}
|
316
|
+
```
|
317
|
+
|
318
|
+
**Flatten**
|
319
|
+
|
320
|
+
Finally, if you want to slurp up all messages into the outermost xeme and delete
|
321
|
+
the nested xemes, use `#flatten`.
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
puts xeme['errors']
|
325
|
+
# => {"id"=>"outer-error"}
|
326
|
+
|
327
|
+
xeme.flatten
|
328
|
+
|
329
|
+
puts xeme['errors']
|
330
|
+
# => {"id"=>"outer-error"}
|
331
|
+
# => {"id"=>"child-error"}
|
332
|
+
```
|
333
|
+
|
334
|
+
|
335
|
+
#### Resolving xemes
|
336
|
+
|
337
|
+
A xeme can contain contradictory information. For example, if `success` is true
|
338
|
+
but there are errors, then the xeme should be considered as failed. If there are
|
339
|
+
promises, then the xeme should not be considered as failed, although `success`
|
340
|
+
may be set as nil.
|
341
|
+
|
342
|
+
The `#resolve` method resolves those contradictions. Generally you won't have to
|
343
|
+
call `#resolve` yourself, but it's worth understanding the rules:
|
344
|
+
|
345
|
+
* A xeme can always be explicitly set to false, regardless of any other
|
346
|
+
considerations. Resolution never changes a `success` of false.
|
347
|
+
|
348
|
+
* If any nested xemes have `success` explicitly set to false, then the outermost
|
349
|
+
xeme will be set to false.
|
350
|
+
|
351
|
+
* If a xeme has errors, or any of it's nested xemes has errors, then it is set
|
352
|
+
to false.
|
353
|
+
|
354
|
+
* If a xeme has promises, or any of its nested xemes do, then it cannot be set
|
355
|
+
to true. If it is already false, then it stays false. Otherwise `success` is set
|
356
|
+
to nil.
|
357
|
+
|
358
|
+
* If any nested xemes have `success` set to nil, then the outermost xeme cannot
|
359
|
+
be set to true.
|
261
360
|
|
262
361
|
## The name
|
263
362
|
|
@@ -267,12 +366,6 @@ also known as Sabine's gull, is a type of gull. See
|
|
267
366
|
[the Wikipedia page](https://en.wikipedia.org/wiki/Sabine's_gull)
|
268
367
|
if you'd like to know more.
|
269
368
|
|
270
|
-
## Install
|
271
|
-
|
272
|
-
```
|
273
|
-
gem install xeme
|
274
|
-
```
|
275
|
-
|
276
369
|
## Author
|
277
370
|
|
278
371
|
Mike O'Sullivan
|
@@ -283,5 +376,4 @@ mike@idocs.com
|
|
283
376
|
| version | date | notes |
|
284
377
|
|---------|--------------|-------------------------------|
|
285
378
|
| 0.1 | Jan 7, 2020 | Initial upload. |
|
286
|
-
| 0
|
287
|
-
| 0.3 | Jan 19, 2020 | Fixed bug in Xeme.from_json() |
|
379
|
+
| 1.0 | May 29, 2023 | Complete overhaul. Not backward compatible. |
|