Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): add p/jeronimoalbi/datastore package #3698

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/datastore.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package datastore

import (
"errors"

"gno.land/p/demo/avl"
)

// TODO: Write unit test for Datastore

// ErrStorageExists indicates that a storage exists with the same name.
var ErrStorageExists = errors.New("a storage with the same name exists")

// Datastore is a store that can contain multiple named storages.
// A storage is a collection of records.
//
// Example usage:
//
// // Create an empty storage to store user records
// var db Datastore
// storage := db.CreateStorage("users")
//
// // Get a storage that has been created before
// // and search a user by record ID.
// storage = db.GetStorage("users")
// user := storage.GetFirst(user.Key())
type Datastore struct {
storages avl.Tree // string(name) -> *Storage
}

// CreateStorage creates a new named storage within the data store.
func (ds *Datastore) CreateStorage(name string, options ...StorageOption) *Storage {
if ds.storages.Has(name) {
return nil
}

s := NewStorage(name, options...)
ds.storages.Set(name, &s)
return &s
}

// HasStorage checks if data store contains a storage with a specific name.
func (ds Datastore) HasStorage(name string) bool {
return ds.storages.Has(name)
}

// GetStorage returns a storage that has been created with a specific name.
// It returns nil when a storage with the specified name is not found.
func (ds Datastore) GetStorage(name string) *Storage {
if v, found := ds.storages.Get(name); found {
return v.(*Storage)
}
return nil
}
78 changes: 78 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/datastore_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package datastore

import (
"testing"

"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

func TestDatastoreCreateStorage(t *testing.T) {
cases := []struct {
name string
storageName string
mustFail bool
setup func(*Datastore)
}{
{
name: "success",
storageName: "users",
},
{
name: "storage exists",
storageName: "users",
mustFail: true,
setup: func(db *Datastore) {
db.CreateStorage("users")
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var db Datastore
if tc.setup != nil {
tc.setup(&db)
}

storage := db.CreateStorage(tc.storageName)

if tc.mustFail {
uassert.Equal(t, nil, storage)
return
}

urequire.NotEqual(t, nil, storage, "storage created")
uassert.Equal(t, tc.storageName, storage.Name())
uassert.True(t, db.HasStorage(tc.storageName))
})
}
}

func TestDatastoreHasStorage(t *testing.T) {
var (
db Datastore
name = "users"
)

uassert.False(t, db.HasStorage(name))

db.CreateStorage(name)
uassert.True(t, db.HasStorage(name))
}

func TestDatastoreGetStorage(t *testing.T) {
var (
db Datastore
name = "users"
)

storage := db.GetStorage(name)
uassert.Equal(t, nil, storage)

db.CreateStorage(name)

storage = db.GetStorage(name)
urequire.NotEqual(t, nil, storage, "storage found")
uassert.Equal(t, name, storage.Name())
}
53 changes: 53 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Package datastore provides support to store multiple collections of records.
//
// It supports the definition of multiple storages, where each one is a collection
// of records. Records can have any number of user defined fields which are added
// dynamically when values are set on a record. These fields can also be renamed
// or removed.
//
// Storages have support for simple schemas that allows users to pre-define fields
// which can optionally have a default value also defined. Default values are
// assigned to new records on creation.
//
// User defined schemas can optionally be strict, which means that records from a
// storage using the schema can only assign values to the pre-defined set of fields.
// In which case, assigning a value to an unknown field would result on an error.
//
// Package also support the definition of custom record indexes. Indexes are used
// by storages to search and iterate records.
// The default index is the ID index but custom single and multi value indexes can
// be defined.
//
// Example usage:
//
// var db datastore.Datastore
//
// // Define a unique case insensitive index for user emails
// emailIdx := datastore.NewIndex("email", func(r datastore.Record) string {
// return r.MustGet("email").(string)
// }).Unique().CaseInsensitive()
//
// // Create a new storage for user records
// storage := db.CreateStorage("users", datastore.WithIndex(emailIdx))
//
// // Add a user with a single "email" field
// user := storage.NewRecord()
// user.Set("email", "foo@bar.org")
//
// // Save to assing user ID and update indexes
// user.Save()
//
// // Find user by email
// user = storage.WithIndex("email").GetFirst("foo@bar.org")
//
// // Find user by ID
// user = storage.GetFirst(user.Key())
//
// // Search user's profile by email in another existing storage
// storage = db.GetStorage("profiles")
// email := user.MustGet("email").(string)
// profile := storage.WithIndex("user").GetFirst(email)
//
// // Delete the profile from the storage and update indexes
// storage.Delete(profile.ID())
package datastore
1 change: 1 addition & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/jeronimoalbi/datastore
100 changes: 100 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/indexes.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package datastore

import (
"gno.land/p/moul/collection"
)

// DefaultIndexOptions defines the default options for new indexes.
const DefaultIndexOptions = collection.DefaultIndex | collection.SparseIndex

type (
// IndexFn defines a type for single value indexing functions.
// This type of function extracts a single string value from
// a record that is then used to index it.
IndexFn func(Record) string

// IndexMultiValueFn defines a type for multi value indexing functions.
// This type of function extracts multiple string values from a
// record that are then used to index it.
IndexMultiValueFn func(Record) []string

// Index defines a type for custom user defined storage indexes.
// Storages are by default indexed by the auto geneated record ID
// but can additionally be indexed by other custom record fields.
Index struct {
name string
options collection.IndexOption
fn interface{}
}
)

// NewIndex creates a new single value index.
//
// Usage example:
//
// // Index a User record by email
// idx := NewIndex("email", func(r Record) string {
// return r.MustGet("email").(string)
// })
func NewIndex(name string, fn IndexFn) Index {
return Index{
name: name,
options: DefaultIndexOptions,
fn: func(v interface{}) string {
return fn(v.(Record))
},
}
}

// NewMultiValueIndex creates a new multi value index.
//
// Usage example:
//
// // Index a Post record by tag
// idx := NewMultiValueIndex("tag", func(r Record) []string {
// return r.MustGet("tags").([]string)
// })
func NewMultiValueIndex(name string, fn IndexMultiValueFn) Index {
return Index{
name: name,
options: DefaultIndexOptions,
fn: func(v interface{}) []string {
return fn(v.(Record))
},
}
}

// Name returns index's name.
func (idx Index) Name() string {
return idx.name
}

// Options returns current index options.
// These options define the index behavior regarding case sensitivity and uniquenes.
func (idx Index) Options() collection.IndexOption {
return idx.options
}

// Func returns the function that storage collections apply
// to each record to get the value to use for indexing it.
func (idx Index) Func() interface{} {
return idx.fn
}

// Unique returns a copy of the index that indexes record values as unique values.
// Returned index contains previous options plus the unique one.
func (idx Index) Unique() Index {
if idx.options&collection.UniqueIndex == 0 {
idx.options |= collection.UniqueIndex
}
return idx
}

// CaseInsensitive returns a copy of the index that indexes record values ignoring casing.
// Returned index contains previous options plus the case insensitivity one.
func (idx Index) CaseInsensitive() Index {
if idx.options&collection.CaseInsensitiveIndex == 0 {
idx.options |= collection.CaseInsensitiveIndex
}
return idx
}
98 changes: 98 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/indexes_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package datastore

import (
"testing"

"gno.land/p/demo/uassert"
"gno.land/p/moul/collection"
)

func TestNewIndex(t *testing.T) {
cases := []struct {
name string
options collection.IndexOption
setup func(Index) Index
}{
{
name: "default",
options: DefaultIndexOptions,
},
{
name: "unique",
options: DefaultIndexOptions | collection.UniqueIndex,
setup: func(idx Index) Index { return idx.Unique() },
},
{
name: "case insensitive",
options: DefaultIndexOptions | collection.CaseInsensitiveIndex,
setup: func(idx Index) Index { return idx.CaseInsensitive() },
},
{
name: "unique case insensitive",
options: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,
setup: func(idx Index) Index { return idx.CaseInsensitive().Unique() },
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
name := "foo"
idx := NewIndex(name, func(Record) string { return "" })

if tc.setup != nil {
idx = tc.setup(idx)
}

uassert.Equal(t, name, idx.Name())
uassert.Equal(t, uint64(tc.options), uint64(idx.Options()))

_, ok := idx.Func().(func(interface{}) string)
uassert.True(t, ok)
})
}
}

func TestNewMultiIndex(t *testing.T) {
cases := []struct {
name string
options collection.IndexOption
setup func(Index) Index
}{
{
name: "default",
options: DefaultIndexOptions,
},
{
name: "unique",
options: DefaultIndexOptions | collection.UniqueIndex,
setup: func(idx Index) Index { return idx.Unique() },
},
{
name: "case insensitive",
options: DefaultIndexOptions | collection.CaseInsensitiveIndex,
setup: func(idx Index) Index { return idx.CaseInsensitive() },
},
{
name: "unique case insensitive",
options: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,
setup: func(idx Index) Index { return idx.CaseInsensitive().Unique() },
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
name := "foo"
idx := NewMultiValueIndex(name, func(Record) []string { return nil })

if tc.setup != nil {
idx = tc.setup(idx)
}

uassert.Equal(t, name, idx.Name())
uassert.Equal(t, uint64(tc.options), uint64(idx.Options()))

_, ok := idx.Func().(func(interface{}) []string)
uassert.True(t, ok)
})
}
}
Loading
Loading