-
Notifications
You must be signed in to change notification settings - Fork 384
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
examples/gno.land/p/jeronimoalbi/datasource/datasource.gno
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,103 @@ | ||
// Package datasource defines generic interfaces for datasources. | ||
// | ||
// Datasources contain a set of records which can optionally be | ||
// taggable. Tags can optionally be used to filter records by taxonomy. | ||
// | ||
// Datasources can help in cases where the data sent during | ||
// communication between different realms needs to be generic | ||
// to avoid direct dependencies. | ||
package datasource | ||
|
||
import "errors" | ||
|
||
// ErrInvalidRecord indicates that a datasource contains invalid records. | ||
var ErrInvalidRecord = errors.New("datasource records is not valid") | ||
|
||
type ( | ||
// Fields defines an interface for read-only fields. | ||
Fields interface { | ||
// Has checks whether a field exists. | ||
Has(name string) bool | ||
|
||
// Get retrieves the value associated with the given field. | ||
Get(name string) (value interface{}, found bool) | ||
} | ||
|
||
// Record defines a datasource record. | ||
Record interface { | ||
// ID returns the unique record's identifier. | ||
ID() string | ||
|
||
// String returns a string representation of the record. | ||
String() string | ||
|
||
// Fields returns record fields and values. | ||
Fields() (Fields, error) | ||
} | ||
|
||
// TaggableRecord defines a datasource record that supports tags. | ||
// Tags can be used to build a taxonomy to filter records by category. | ||
TaggableRecord interface { | ||
// Tags returns a list of tags for the record. | ||
Tags() []string | ||
} | ||
|
||
// ContentRecord defines a datasource record that can return content. | ||
ContentRecord interface { | ||
// Content returns the record content. | ||
Content() (string, error) | ||
} | ||
|
||
// Iterator defines an iterator of datasource records. | ||
Iterator interface { | ||
// Next returns true when a new record is available. | ||
Next() bool | ||
|
||
// Err returns any error raised when reading records. | ||
Err() error | ||
|
||
// Record returns the current record. | ||
Record() Record | ||
} | ||
|
||
// Datasource defines a generic datasource. | ||
Datasource interface { | ||
// Records returns a new datasource records iterator. | ||
Records(Query) Iterator | ||
|
||
// Size returns the total number of records in the datasource. | ||
// When -1 is returned it means datasource doesn't support size. | ||
Size() int | ||
|
||
// Record returns a single datasource record. | ||
Record(id string) (Record, error) | ||
} | ||
) | ||
|
||
// NewIterator returns a new record iterator for a datasource query. | ||
func NewIterator(ds Datasource, options ...QueryOption) Iterator { | ||
return ds.Records(NewQuery(options...)) | ||
} | ||
|
||
// QueryRecords return a slice of records for a datasource query. | ||
func QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) { | ||
var ( | ||
records []Record | ||
query = NewQuery(options...) | ||
iter = ds.Records(query) | ||
) | ||
|
||
for i := 0; i < query.Count && iter.Next(); i++ { | ||
r := iter.Record() | ||
if r == nil { | ||
return nil, ErrInvalidRecord | ||
} | ||
|
||
records = append(records, r) | ||
} | ||
|
||
if err := iter.Err(); err != nil { | ||
return nil, err | ||
} | ||
return records, nil | ||
} |
171 changes: 171 additions & 0 deletions
171
examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno
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,171 @@ | ||
package datasource | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"gno.land/p/demo/uassert" | ||
"gno.land/p/demo/urequire" | ||
) | ||
|
||
func TestNewIterator(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
records []Record | ||
err error | ||
}{ | ||
{ | ||
name: "ok", | ||
records: []Record{ | ||
testRecord{id: "1"}, | ||
testRecord{id: "2"}, | ||
testRecord{id: "3"}, | ||
}, | ||
}, | ||
{ | ||
name: "error", | ||
err: errors.New("test"), | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
// Arrange | ||
ds := testDatasource{ | ||
records: tc.records, | ||
err: tc.err, | ||
} | ||
|
||
// Act | ||
iter := NewIterator(ds) | ||
|
||
// Assert | ||
if tc.err != nil { | ||
uassert.ErrorIs(t, tc.err, iter.Err()) | ||
return | ||
} | ||
|
||
uassert.NoError(t, iter.Err()) | ||
|
||
for i := 0; iter.Next(); i++ { | ||
r := iter.Record() | ||
urequire.NotEqual(t, nil, r, "valid record") | ||
urequire.True(t, i < len(tc.records), "iteration count") | ||
uassert.Equal(t, tc.records[i].ID(), r.ID()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestQueryRecords(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
records []Record | ||
recordCount int | ||
options []QueryOption | ||
err error | ||
}{ | ||
{ | ||
name: "ok", | ||
records: []Record{ | ||
testRecord{id: "1"}, | ||
testRecord{id: "2"}, | ||
testRecord{id: "3"}, | ||
}, | ||
recordCount: 3, | ||
}, | ||
{ | ||
name: "with count", | ||
options: []QueryOption{WithCount(2)}, | ||
records: []Record{ | ||
testRecord{id: "1"}, | ||
testRecord{id: "2"}, | ||
testRecord{id: "3"}, | ||
}, | ||
recordCount: 2, | ||
}, | ||
{ | ||
name: "invalid record", | ||
records: []Record{ | ||
testRecord{id: "1"}, | ||
nil, | ||
testRecord{id: "3"}, | ||
}, | ||
err: ErrInvalidRecord, | ||
}, | ||
{ | ||
name: "iterator error", | ||
records: []Record{ | ||
testRecord{id: "1"}, | ||
testRecord{id: "3"}, | ||
}, | ||
err: errors.New("test"), | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
// Arrange | ||
ds := testDatasource{ | ||
records: tc.records, | ||
err: tc.err, | ||
} | ||
|
||
// Act | ||
records, err := QueryRecords(ds, tc.options...) | ||
|
||
// Assert | ||
if tc.err != nil { | ||
uassert.ErrorIs(t, tc.err, err) | ||
return | ||
} | ||
|
||
uassert.NoError(t, err) | ||
|
||
urequire.Equal(t, tc.recordCount, len(records), "record count") | ||
for i, r := range records { | ||
urequire.NotEqual(t, nil, r, "valid record") | ||
uassert.Equal(t, tc.records[i].ID(), r.ID()) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
type testDatasource struct { | ||
records []Record | ||
err error | ||
} | ||
|
||
func (testDatasource) Size() int { return -1 } | ||
func (testDatasource) Record(string) (Record, error) { return nil, nil } | ||
func (ds testDatasource) Records(Query) Iterator { return &testIter{records: ds.records, err: ds.err} } | ||
|
||
type testRecord struct { | ||
id string | ||
fields Fields | ||
err error | ||
} | ||
|
||
func (r testRecord) ID() string { return r.id } | ||
func (r testRecord) String() string { return "str" + r.id } | ||
func (r testRecord) Fields() (Fields, error) { return r.fields, r.err } | ||
|
||
type testIter struct { | ||
index int | ||
records []Record | ||
current Record | ||
err error | ||
} | ||
|
||
func (it testIter) Err() error { return it.err } | ||
func (it testIter) Record() Record { return it.current } | ||
|
||
func (it *testIter) Next() bool { | ||
count := len(it.records) | ||
if it.err != nil || count == 0 || it.index >= count { | ||
return false | ||
} | ||
it.current = it.records[it.index] | ||
it.index++ | ||
return true | ||
} |
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 @@ | ||
module gno.land/p/jeronimoalbi/datasource |
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,70 @@ | ||
package datasource | ||
|
||
import "gno.land/p/demo/avl" | ||
|
||
// DefaultQueryRecords defines the default number of records returned by queries. | ||
const DefaultQueryRecords = 50 | ||
|
||
var defaultQuery = Query{Count: DefaultQueryRecords} | ||
|
||
type ( | ||
// QueryOption configures datasource queries. | ||
QueryOption func(*Query) | ||
|
||
// Query contains datasource query options. | ||
Query struct { | ||
// Offset of the first record to return during iteration. | ||
Offset int | ||
|
||
// Count contains the number to records that query should return. | ||
Count int | ||
|
||
// Tag contains a tag to use as filter for the records. | ||
Tag string | ||
|
||
// Filters contains optional query filters by field value. | ||
Filters avl.Tree | ||
} | ||
) | ||
|
||
// WithOffset configures query to return records starting from an offset. | ||
func WithOffset(offset int) QueryOption { | ||
return func(q *Query) { | ||
q.Offset = offset | ||
} | ||
} | ||
|
||
// WithCount configures the number of records that query returns. | ||
func WithCount(count int) QueryOption { | ||
return func(q *Query) { | ||
if count < 1 { | ||
count = DefaultQueryRecords | ||
} | ||
q.Count = count | ||
} | ||
} | ||
|
||
// ByTag configures query to filter by tag. | ||
func ByTag(tag string) QueryOption { | ||
return func(q *Query) { | ||
q.Tag = tag | ||
} | ||
} | ||
|
||
// WithFilter assigns a new filter argument to a query. | ||
// This option can be used multiple times if more than one | ||
// filter has to be given to the query. | ||
func WithFilter(field string, value interface{}) QueryOption { | ||
return func(q *Query) { | ||
q.Filters.Set(field, value) | ||
} | ||
} | ||
|
||
// NewQuery creates a new datasource query. | ||
func NewQuery(options ...QueryOption) Query { | ||
q := defaultQuery | ||
for _, apply := range options { | ||
apply(&q) | ||
} | ||
return q | ||
} |
Oops, something went wrong.