Skip to content

Commit

Permalink
Merge branch 'master' into fix/2628
Browse files Browse the repository at this point in the history
  • Loading branch information
omarsy authored Dec 11, 2024
2 parents 88dae32 + 6f48a5b commit 59a7d85
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 0 deletions.
103 changes: 103 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datasource/datasource.gno
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 examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno
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
}
1 change: 1 addition & 0 deletions examples/gno.land/p/jeronimoalbi/datasource/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/jeronimoalbi/datasource
70 changes: 70 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datasource/query.gno
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
}
Loading

0 comments on commit 59a7d85

Please sign in to comment.