Skip to content

Commit

Permalink
Merge pull request #4 from FreeLeh/improve-docs
Browse files Browse the repository at this point in the history
Write Godoc
  • Loading branch information
fatanugraha authored Aug 22, 2022
2 parents f9f89e5 + 9c6dfdc commit 757ce9b
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 107 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ auth, err := auth.NewServiceFromFile(

**Explanations:**

1. The `service_account_json` can be obtained by following the steps in this [Google OAuth2 page](https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount). The JSON file of interest is **the downloaded file after creating a new service account key**.
1. The `service_account_json` can be obtained by following the steps in this [Google Service Account page](https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount). The JSON file of interest is **the downloaded file after creating a new service account key**.
2. The `scopes` tells Google what your application can do to your spreadsheets (`auth.GoogleSheetsReadOnly`, `auth.GoogleSheetsWriteOnly`, or `auth.GoogleSheetsReadWrite`).

If you want to understand the details, you can start from this [Google Service Account page](https://developers.google.com/identity/protocols/oauth2/service-account).
Expand Down
14 changes: 11 additions & 3 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import "errors"

const basicCodecPrefix string = "!"

type BasicCodec struct{}
// basicCodec encodes and decodes any bytes data using a very simple encoding rule.
// A prefix (an exclamation mark "!") will be attached to the raw bytes data.
//
// This allows the library to differentiate between empty raw bytes provided by the client from
// getting an empty data from Google Sheets API.
type basicCodec struct{}

func (c *BasicCodec) Encode(value []byte) (string, error) {
// Encode encodes the given raw bytes by using an exclamation mark "!" as a prefix.
func (c *basicCodec) Encode(value []byte) (string, error) {
return basicCodecPrefix + string(value), nil
}

func (c *BasicCodec) Decode(value string) ([]byte, error) {
// Decode converts the string data read from Google Sheet into raw bytes data after removing the
// exclamation mark "!" prefix.
func (c *basicCodec) Decode(value string) ([]byte, error) {
if len(value) == 0 {
return nil, errors.New("basic decode fail: empty string")
}
Expand Down
4 changes: 2 additions & 2 deletions codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestBasicCodecEncode(t *testing.T) {
expected: "!" + naValue,
},
}
codec := &BasicCodec{}
codec := &basicCodec{}

for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestBasicCodecDecode(t *testing.T) {
hasErr: false,
},
}
codec := &BasicCodec{}
codec := &basicCodec{}

for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions google/auth/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package auth provides general Google authentication implementation agnostic to what specific Google services or
// resources are used. Implementations in this package generate a https://pkg.go.dev/net/http#Client that can be used
// to access Google REST APIs seamlessly. Authentications will be handled automatically, including refreshing
//the access token when necessary.
package auth
1 change: 1 addition & 0 deletions google/auth/models.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package auth

// Scopes encapsulates a list of Google resources scopes to request during authentication step.
type Scopes []string

var (
Expand Down
20 changes: 20 additions & 0 deletions google/auth/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,38 @@ const (
stateLength = 32
)

// OAuth2Config defines a list of configurations that can be used to customise how the Google OAuth2 flow works.
type OAuth2Config struct {
// HTTPClient allows the client to customise the HTTP client used to perform the REST API calls.
// This will be useful if you want to have a more granular control over the HTTP client (e.g. using a connection pool).
HTTPClient *http.Client
}

// OAuth2 takes in OAuth2 relevant information and sets up *http.Client that can be used to access
// Google APIs seamlessly. Authentications will be handled automatically, including refreshing the access token
// when necessary.
type OAuth2 struct {
googleAuthClient *http.Client
}

// HTTPClient returns a Google OAuth2 authenticated *http.Client that can be used to access Google APIs.
func (o *OAuth2) HTTPClient() *http.Client {
return o.googleAuthClient
}

// NewOAuth2FromFile creates an OAuth2 instance by reading the OAuth2 related information from a secret file.
//
// The "secretFilePath" is referring to the OAuth2 credentials JSON file that can be obtained by
// creating a new OAuth2 credentials in https://console.cloud.google.com/apis/credentials.
// You can put any link for the redirection URL field.
//
// The "credsFilePath" is referring to a file where the generated access and refresh token will be cached.
// This file will be created automatically once the OAuth2 authentication is successful.
//
// The "scopes" tells Google what your application can do to your spreadsheets.
//
// Note that since this is an OAuth2 server flow, human interaction will be needed for the very first authentication.
// During the OAuth2 flow, you will be asked to click a generated URL in the terminal.
func NewOAuth2FromFile(secretFilePath string, credsFilePath string, scopes Scopes, config OAuth2Config) (*OAuth2, error) {
rawAuthConfig, err := ioutil.ReadFile(secretFilePath)
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions google/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,32 @@ import (
"golang.org/x/oauth2/google"
)

// ServiceConfig defines a list of configurations that can be used to customise how the Google
// service account authentication flow works.
type ServiceConfig struct {
// HTTPClient allows the client to customise the HTTP client used to perform the REST API calls.
// This will be useful if you want to have a more granular control over the HTTP client (e.g. using a connection pool).
HTTPClient *http.Client
}

// Service takes in service account relevant information and sets up *http.Client that can be used to access
// Google APIs seamlessly. Authentications will be handled automatically, including refreshing the access token
// when necessary.
type Service struct {
googleAuthClient *http.Client
}

// HTTPClient returns a Google OAuth2 authenticated *http.Client that can be used to access Google APIs.
func (s *Service) HTTPClient() *http.Client {
return s.googleAuthClient
}

// NewServiceFromFile creates a Service instance by reading the Google service account related information from a file.
//
// The "filePath" is referring to the service account JSON file that can be obtained by
// creating a new service account credentials in https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount.
//
// The "scopes" tells Google what your application can do to your spreadsheets.
func NewServiceFromFile(filePath string, scopes Scopes, config ServiceConfig) (*Service, error) {
authConfig, err := ioutil.ReadFile(filePath)
if err != nil {
Expand All @@ -27,6 +41,8 @@ func NewServiceFromFile(filePath string, scopes Scopes, config ServiceConfig) (*
return NewServiceFromJSON(authConfig, scopes, config)
}

// NewServiceFromJSON works exactly the same as NewServiceFromFile, but instead of reading from a file, the raw content
// of the Google service account JSON file is provided directly.
func NewServiceFromJSON(raw []byte, scopes Scopes, config ServiceConfig) (*Service, error) {
c, err := google.JWTConfigFromJSON(raw, scopes...)
if err != nil {
Expand Down
91 changes: 42 additions & 49 deletions kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,17 @@ import (
"github.com/FreeLeh/GoFreeLeh/internal/google/sheets"
)

// GoogleSheetKVStoreConfig defines a list of configurations that can be used to customise how the GoogleSheetKVStore works.
type GoogleSheetKVStoreConfig struct {
Mode KVMode
codec Codec
}

/*
There are 2 formats of the same KV storage using Google Sheet:
- Default -> like a normal KV store, each key only appears once in the sheet.
- Append only -> each key update will be added as a new row, there maybe >1 rows for the same keys. The latest added row for a key is the latest value.
## APPEND ONLY MODE
The structure is as follows:
key | value | timestamp
k1 | v1 | 1
k2 | v2 | 2
k3 | v3 | 3
k2 | v22 | 4 --> Set(k2, v22)
k3 | v32 | 5 --> Set(k3, v32)
k2 | | 6 --> Delete(k2) -> value is set to an empty string
The logic for Set() is as simple as appending a new row at the end of the current sheet with the latest value and the timestamp in milliseconds.
The logic for Delete() is basically Set(key, "").
The logic for Get() is more complicated.
=VLOOKUP(key, SORT(<full_table_range>, 3, FALSE), 2, FALSE)
The full table range can be assumed to be A1:C5000000.
The integer "3" is referring to the timestamp column (the third column of the table).
The FALSE inside the SORT() means sort in descending order.
The FALSE inside the VLOOKUP() means we consider the table as non-sorted (so that we can take the first row which has the latest timestamp as the final value).
The integer "2" is referring to which column we want to return from VLOOKUP(), which is referring to the value column.
If the value returned is either "#N/A" or "", that means the key is not found or already deleted.
## DEFAULT MODE
The structure is as follows:
key | value | timestamp
k1 | v1 | 1
k2 | v2 | 2
k3 | v3 | 3
The logic for Set() is Get() + (Append(OVERWRITE_MODE) if not exists OR Update if already exists).
The logic for Delete() is Get() + Clear().
The logic for Get() is just a simple VLOOKUP without any sorting involved (unlike the APPEND ONLY mode).
Here we assume there cannot be any race condition that leads to two rows with the same key.
*/
// GoogleSheetKVStore encapsulates key-value store functionality on top of a Google Sheet.
//
// There are 2 operation modes for the key-value store: default and append only mode.
//
// For more details on how they differ, please read the explanations for each method or the TODO(edocsss) protocol page.
type GoogleSheetKVStore struct {
wrapper sheetsWrapper
spreadsheetID string
Expand All @@ -70,6 +28,18 @@ type GoogleSheetKVStore struct {
config GoogleSheetKVStoreConfig
}

// Get retrieves the value associated with the given key.
// If the key exists in the store, the raw bytes value and no error will be returned.
// If the key does not exist in the store, a nil []byte and a wrapped ErrKeyNotFound will be returned.
//
// In default mode,
// - There will be only one row with the given key. It will return the value for that in that row.
// - There is only 1 API call behind the scene.
//
// In append only mode,
// - As there could be multiple rows with the same key, we need to only use the latest row as it contains the last updated value.
// - Note that deletion using append only mode results in a new row with a tombstone value. This method will also recognise and handle such cases.
// - There is only 1 API call behind the scene.
func (s *GoogleSheetKVStore) Get(ctx context.Context, key string) ([]byte, error) {
query := fmt.Sprintf(kvGetDefaultQueryTemplate, key, getA1Range(s.sheetName, defaultKVTableRange))
if s.config.Mode == KVModeAppendOnly {
Expand All @@ -96,6 +66,16 @@ func (s *GoogleSheetKVStore) Get(ctx context.Context, key string) ([]byte, error
return s.config.codec.Decode(value.(string))
}

// Set inserts the key-value pair into the key-value store.
//
// In default mode,
// - If the key is not in the store, `Set` will create a new row and store the key value pair there.
// - If the key is in the store, `Set` will update the previous row with the new value and timestamp.
// - There are exactly 2 API calls behind the scene: getting the row for the key and creating/updating with the given key value data.
//
// In append only mode,
// - It always creates a new row at the bottom of the sheet with the latest value and timestamp.
// - There is only 1 API call behind the scene.
func (s *GoogleSheetKVStore) Set(ctx context.Context, key string, value []byte) error {
encoded, err := s.config.codec.Encode(value)
if err != nil {
Expand Down Expand Up @@ -169,6 +149,16 @@ func (s *GoogleSheetKVStore) findKeyA1Range(ctx context.Context, key string) (sh
return sheets.NewA1Range(a1Range), nil
}

// Delete deletes the given key from the key-value store.
//
// In default mode,
// - If the key is not in the store, it will not do anything.
// - If the key is in the store, it will remove that row.
// - There are up to 2 API calls behind the scene: getting the row for the key and remove the row (if the key exists).
//
// In append only mode,
// - It creates a new row at the bottom of the sheet with a tombstone value and timestamp.
// - There is only 1 API call behind the scene.
func (s *GoogleSheetKVStore) Delete(ctx context.Context, key string) error {
if s.config.Mode == KVModeAppendOnly {
return s.deleteAppendOnly(ctx, key)
Expand All @@ -193,11 +183,14 @@ func (s *GoogleSheetKVStore) deleteDefault(ctx context.Context, key string) erro
return err
}

// Close cleans up all held resources like the scratchpad cell booked for this specific GoogleSheetKVStore instance.
func (s *GoogleSheetKVStore) Close(ctx context.Context) error {
_, err := s.wrapper.Clear(ctx, s.spreadsheetID, []string{s.scratchpadLocation.Original})
return err
}

// NewGoogleSheetKVStore creates an instance of the key-value store with the given configuration.
// It will also try to create the sheet, in case it does not exist yet.
func NewGoogleSheetKVStore(
auth sheets.AuthClient,
spreadsheetID string,
Expand Down Expand Up @@ -233,6 +226,6 @@ func NewGoogleSheetKVStore(
}

func applyGoogleSheetKVStoreConfig(config GoogleSheetKVStoreConfig) GoogleSheetKVStoreConfig {
config.codec = &BasicCodec{}
config.codec = &basicCodec{}
return config
}
24 changes: 19 additions & 5 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ import (
"github.com/FreeLeh/GoFreeLeh/internal/google/sheets"
)

// KVMode defines the mode of the key value store.
// For more details, please read the README file.
type KVMode int

type OrderBy string

const (
KVModeDefault KVMode = 0
KVModeAppendOnly KVMode = 1
)

OrderByAsc OrderBy = "ASC"
OrderByDesc OrderBy = "DESC"

const (
// Currently limited to 26.
// Otherwise, the sheet creation must extend the column as well to make the rowGetIndicesQueryTemplate formula works.
// TODO(edocsss): add an option to extend the number of columns.
Expand Down Expand Up @@ -47,9 +46,12 @@ const (
rowIdxFormula = "=ROW()"
)

// ErrKeyNotFound is returned only for the key-value store and when the key does not exist.
var (
ErrKeyNotFound = errors.New("error key not found")
)

var (
defaultRowHeaderRange = "A1:" + generateColumnName(maxColumn-1) + "1"
defaultRowFullTableRange = "A2:" + generateColumnName(maxColumn-1)
rowDeleteRangeTemplate = "A%d:" + generateColumnName(maxColumn-1) + "%d"
Expand All @@ -59,6 +61,8 @@ var (
googleSheetSelectStmtStringKeyword = regexp.MustCompile("^(date|datetime|timeofday)")
)

// Codec is an interface for encoding and decoding the data provided by the client.
// At the moment, only key-value store requires data encoding.
type Codec interface {
Encode(value []byte) (string, error)
Decode(value string) ([]byte, error)
Expand Down Expand Up @@ -100,6 +104,16 @@ func (m colsMapping) ColIdxNameMap() map[string]string {
return result
}

// OrderBy defines the type of column ordering used for GoogleSheetRowStore.Select().
type OrderBy string

const (
OrderByAsc OrderBy = "ASC"
OrderByDesc OrderBy = "DESC"
)

// ColumnOrderBy defines what ordering is required for a particular column.
// This is used for GoogleSheetRowStore.Select().
type ColumnOrderBy struct {
Column string
OrderBy OrderBy
Expand Down
Loading

0 comments on commit 757ce9b

Please sign in to comment.