-
Notifications
You must be signed in to change notification settings - Fork 249
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(sdk/go): Refactor the key/value Go SDK to be more idiomatic #1845
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// Package kv provides access to key value stores within Spin | ||
// components. | ||
package kv | ||
|
||
// #include "key-value.h" | ||
import "C" | ||
import ( | ||
"errors" | ||
"fmt" | ||
"unsafe" | ||
) | ||
|
||
// Store is the Key/Value backend storage. | ||
type Store struct { | ||
name string | ||
active bool | ||
ptr C.key_value_store_t | ||
} | ||
|
||
// OpenStore creates a new instance of Store and opens a connection. | ||
func OpenStore(name string) (*Store, error) { | ||
s := &Store{name: name} | ||
if err := s.open(); err != nil { | ||
return nil, err | ||
} | ||
return s, nil | ||
} | ||
|
||
// Close terminates the connection to Store. | ||
func (s *Store) Close() { | ||
if s.active { | ||
C.key_value_close(C.uint32_t(s.ptr)) | ||
} | ||
s.active = false | ||
} | ||
|
||
// Get retrieves a value from Store. | ||
func (s *Store) Get(key string) ([]byte, error) { | ||
ckey := toCStr(key) | ||
var ret C.key_value_expected_list_u8_error_t | ||
C.key_value_get(C.uint32_t(s.ptr), &ckey, &ret) | ||
if ret.is_err { | ||
return nil, toErr((*C.key_value_error_t)(unsafe.Pointer(&ret.val))) | ||
} | ||
list := (*C.key_value_list_u8_t)(unsafe.Pointer(&ret.val)) | ||
return C.GoBytes(unsafe.Pointer(list.ptr), C.int(list.len)), nil | ||
} | ||
|
||
// Delete removes a value from Store. | ||
func (s *Store) Delete(key string) error { | ||
ckey := toCStr(key) | ||
var ret C.key_value_expected_unit_error_t | ||
C.key_value_delete(C.uint32_t(s.ptr), &ckey, &ret) | ||
if ret.is_err { | ||
return toErr((*C.key_value_error_t)(unsafe.Pointer(&ret.val))) | ||
} | ||
return nil | ||
} | ||
|
||
// Set creates a new key/value in Store. | ||
func (s *Store) Set(key string, value []byte) error { | ||
ckey := toCStr(key) | ||
cbytes := toCBytes(value) | ||
var ret C.key_value_expected_unit_error_t | ||
C.key_value_set(C.uint32_t(s.ptr), &ckey, &cbytes, &ret) | ||
if ret.is_err { | ||
return toErr((*C.key_value_error_t)(unsafe.Pointer(&ret.val))) | ||
} | ||
return nil | ||
} | ||
|
||
// Exists checks if a key exists within Store. | ||
func (s *Store) Exists(key string) (bool, error) { | ||
ckey := toCStr(key) | ||
var ret C.key_value_expected_bool_error_t | ||
C.key_value_exists(C.uint32_t(s.ptr), &ckey, &ret) | ||
if ret.is_err { | ||
return false, toErr((*C.key_value_error_t)(unsafe.Pointer(&ret.val))) | ||
} | ||
return *(*bool)(unsafe.Pointer(&ret.val)), nil | ||
} | ||
|
||
func (s *Store) open() error { | ||
if s.active { | ||
return nil | ||
} | ||
cname := toCStr(s.name) | ||
var ret C.key_value_expected_store_error_t | ||
C.key_value_open(&cname, &ret) | ||
if ret.is_err { | ||
return toErr((*C.key_value_error_t)(unsafe.Pointer(&ret.val))) | ||
} | ||
s.ptr = *(*C.key_value_store_t)(unsafe.Pointer(&ret.val)) | ||
s.active = true | ||
return nil | ||
} | ||
|
||
func toCBytes(x []byte) C.key_value_list_u8_t { | ||
return C.key_value_list_u8_t{ptr: (*C.uint8_t)(unsafe.Pointer(&x[0])), len: C.size_t(len(x))} | ||
} | ||
|
||
func toCStr(x string) C.key_value_string_t { | ||
return C.key_value_string_t{ptr: C.CString(x), len: C.size_t(len(x))} | ||
} | ||
|
||
func fromCStrList(list *C.key_value_list_string_t) []string { | ||
var result []string | ||
|
||
listLen := int(list.len) | ||
slice := unsafe.Slice(list.ptr, listLen) | ||
for i := 0; i < listLen; i++ { | ||
str := slice[i] | ||
result = append(result, C.GoStringN(str.ptr, C.int(str.len))) | ||
} | ||
|
||
return result | ||
} | ||
|
||
func toErr(err *C.key_value_error_t) error { | ||
switch err.tag { | ||
case 0: | ||
return errors.New("store table full") | ||
case 1: | ||
return errors.New("no such store") | ||
case 2: | ||
return errors.New("access denied") | ||
case 3: | ||
return errors.New("invalid store") | ||
case 4: | ||
return errors.New("no such key") | ||
Comment on lines
+122
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should be exported There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was going to include that in a separate PR to make all the SDK's consistent. I wasn't sure yet if a custom error type would be the way to go. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, that's fine. As a general approach I would probably do something like: // Named const for payload-less variants
var ErrAccessDenied = errors.New("access denied")
// Error type for most payload-ful variants (not KV)
type ImaginaryError string
// Anonymous errors for `other(string)` variants
errors.New(otherString) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about something like... type Error struct {
Code int
Message string
} Stdlib example https://github.com/golang/go/blob/master/src/net/textproto/textproto.go#L35 But it's still a little awkward because Code is an internal value that has no real meaning to the user There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends on whether we care more about trying to closely match the WIT or trying to write a "idiomatic" Go lib I think. For the latter I would say separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taking no-payload: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sold. I was getting distracted by the wit. Thanks for the journey. |
||
case 5: | ||
str := (*C.key_value_string_t)(unsafe.Pointer(&err.val)) | ||
return fmt.Errorf("io error: %s", C.GoStringN(str.ptr, C.int(str.len))) | ||
default: | ||
return fmt.Errorf("unrecognized error: %v", err.tag) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to start only building these when building for WASI and otherwise provide a no-op stub? It'll make building mixed spin/non-spin codebases easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems fine in theory. Would you copy all the function stubs or is there something to automate that?