-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from invopop/workflows-errors
Support filtering workflows by schema and advanced error parsing
- Loading branch information
Showing
13 changed files
with
593 additions
and
160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package invopop | ||
|
||
import ( | ||
"encoding/json" | ||
"strings" | ||
) | ||
|
||
// Dict helps manage a nested map of strings to either messages or | ||
// or other dictionaries. This is useful for accessing error messages | ||
// provided by endpoints that include a "fields" property. | ||
// | ||
// This is based on the Dict model included by the | ||
// [ctxi18n](https://github.com/invopop/ctxi18n) project. | ||
type Dict struct { | ||
msg string | ||
entries map[string]*Dict | ||
} | ||
|
||
// NewDict instantiates a new dict object. | ||
func NewDict() *Dict { | ||
return &Dict{ | ||
entries: make(map[string]*Dict), | ||
} | ||
} | ||
|
||
// Add adds a new key value pair to the dictionary. | ||
func (d *Dict) Add(key string, value any) { | ||
switch v := value.(type) { | ||
case string: | ||
d.entries[key] = &Dict{msg: v} | ||
case map[string]any: | ||
nd := NewDict() | ||
for k, row := range v { | ||
nd.Add(k, row) | ||
} | ||
d.entries[key] = nd | ||
case *Dict: | ||
d.entries[key] = v | ||
default: | ||
// ignore | ||
} | ||
} | ||
|
||
// Message returns the dictionary message or an empty string | ||
// if the dictionary is nil. | ||
func (d *Dict) Message() string { | ||
if d == nil { | ||
return "" | ||
} | ||
return d.msg | ||
} | ||
|
||
// Get recursively retrieves the dictionary at the provided key location. | ||
func (d *Dict) Get(key string) *Dict { | ||
if d == nil { | ||
return nil | ||
} | ||
if key == "" { | ||
return nil | ||
} | ||
n := strings.SplitN(key, ".", 2) | ||
entry, ok := d.entries[n[0]] | ||
if !ok { | ||
return nil | ||
} | ||
if len(n) == 1 { | ||
return entry | ||
} | ||
return entry.Get(n[1]) | ||
} | ||
|
||
// Merge combines the entries of the second dictionary into this one. If a | ||
// key is duplicated in the second diction, the original value takes priority. | ||
func (d *Dict) Merge(d2 *Dict) { | ||
if d2 == nil { | ||
return | ||
} | ||
if d.entries == nil { | ||
d.entries = make(map[string]*Dict) | ||
} | ||
for k, v := range d2.entries { | ||
if d.entries[k] == nil { | ||
d.entries[k] = v | ||
continue | ||
} | ||
d.entries[k].Merge(v) | ||
} | ||
} | ||
|
||
// Flatten returns a simple flat map of the dictionary entries. This might make | ||
// it easier to list out all the error messages for user interfaces. | ||
func (d *Dict) Flatten() map[string]string { | ||
if d == nil { | ||
return nil | ||
} | ||
if d.msg != "" { | ||
return map[string]string{"": d.msg} | ||
} | ||
m := make(map[string]string) | ||
for k, v := range d.entries { | ||
for kk, vv := range v.Flatten() { | ||
x := k | ||
if kk != "" { | ||
x += "." + kk | ||
} | ||
m[x] = vv | ||
} | ||
} | ||
return m | ||
} | ||
|
||
// UnmarshalJSON attempts to load the dictionary data from a JSON byte slice. | ||
func (d *Dict) UnmarshalJSON(data []byte) error { | ||
if len(data) == 0 { | ||
return nil | ||
} | ||
if data[0] == '"' { | ||
d.msg = string(data[1 : len(data)-1]) | ||
return nil | ||
} | ||
d.entries = make(map[string]*Dict) | ||
return json.Unmarshal(data, &d.entries) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package invopop | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDictUnmarshalJSON(t *testing.T) { | ||
ex := `{ | ||
"foo": "bar", | ||
"data": { | ||
"supplier": { | ||
"emails": { | ||
"0": { | ||
"addr": "must be a valid email address" | ||
} | ||
} | ||
} | ||
} | ||
}` | ||
dict := new(Dict) | ||
err := json.Unmarshal([]byte(ex), dict) | ||
require.NoError(t, err) | ||
assert.Equal(t, "bar", dict.Get("foo").Message()) | ||
assert.Equal(t, "must be a valid email address", dict.Get("data.supplier.emails.0.addr").Message()) | ||
assert.Empty(t, dict.Get("data.missing").Message()) | ||
assert.Empty(t, dict.Get("random").Message()) | ||
} | ||
|
||
func TestDictFlatten(t *testing.T) { | ||
ex := `{ | ||
"foo": "bar", | ||
"data": { | ||
"supplier": { | ||
"emails": { | ||
"0": { | ||
"addr": "must be a valid email address" | ||
} | ||
} | ||
} | ||
} | ||
}` | ||
dict := new(Dict) | ||
err := json.Unmarshal([]byte(ex), dict) | ||
require.NoError(t, err) | ||
out := dict.Flatten() | ||
assert.Equal(t, "bar", out["foo"]) | ||
assert.Equal(t, "must be a valid email address", out["data.supplier.emails.0.addr"]) | ||
} | ||
|
||
func TestDictAdd(t *testing.T) { | ||
d := NewDict() | ||
assert.Nil(t, d.Get("")) | ||
d.Add("foo", "bar") | ||
assert.Equal(t, "bar", d.Get("foo").Message()) | ||
|
||
d.Add("plural", map[string]any{ | ||
"zero": "no mice", | ||
"one": "%s mouse", | ||
"other": "%s mice", | ||
}) | ||
assert.Equal(t, "no mice", d.Get("plural.zero").Message()) | ||
assert.Equal(t, "%s mice", d.Get("plural.other").Message()) | ||
|
||
d.Add("bad", 10) // ignore | ||
assert.Nil(t, d.Get("bad")) | ||
|
||
d.Add("self", d) | ||
assert.Equal(t, "bar", d.Get("self.foo").Message()) | ||
} | ||
|
||
func TestDictMerge(t *testing.T) { | ||
ex := `{ | ||
"foo": "bar", | ||
"baz": { | ||
"qux": "quux", | ||
"plural": { | ||
"zero": "no mice", | ||
"one": "%s mouse", | ||
"other": "%s mice" | ||
} | ||
} | ||
}` | ||
d1 := new(Dict) | ||
require.NoError(t, json.Unmarshal([]byte(ex), d1)) | ||
|
||
ex2 := `{ | ||
"foo": "baz", | ||
"extra": "value" | ||
}` | ||
d2 := new(Dict) | ||
require.NoError(t, json.Unmarshal([]byte(ex2), d2)) | ||
|
||
d1.Merge(nil) // does nothing | ||
|
||
d3 := new(Dict) | ||
d3.Merge(d2) | ||
assert.Equal(t, "value", d3.Get("extra").Message()) | ||
|
||
d1.Merge(d2) | ||
assert.Equal(t, "bar", d1.Get("foo").Message(), "should not overwrite") | ||
assert.Equal(t, "value", d1.Get("extra").Message()) | ||
} |
Oops, something went wrong.