Compare commits
3 Commits
99b57c88b0
...
v0.0.7
| Author | SHA1 | Date | |
|---|---|---|---|
| de11a0214b | |||
| 936df98cc4 | |||
| 56b264fcb3 |
@@ -7,7 +7,7 @@ RUN apk add build-base git go sqlite vim shellcheck curl jq sudo bash docker
|
||||
# Busybox `less` doesn't appear to support colors (makes git diff lose color)
|
||||
RUN apk add less
|
||||
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v2.0.2
|
||||
RUN curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b /usr/local/bin v2.14.0
|
||||
RUN GOBIN=/usr/local/bin go install git.offline-twitter.com/offline-labs/gocheckout@v0.0.2
|
||||
|
||||
COPY etc/group /etc/group
|
||||
|
||||
51
pkg/db/vsc.go
Normal file
51
pkg/db/vsc.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const separator = "\x1f"
|
||||
|
||||
type VSC []string
|
||||
|
||||
// MarshalJSON replaces `nil` with `[]` for hygiene.
|
||||
func (vs VSC) MarshalJSON() ([]byte, error) {
|
||||
if vs == nil {
|
||||
vs = VSC{}
|
||||
}
|
||||
return json.Marshal([]string(vs)) //nolint: wrapcheck // Passthrough
|
||||
}
|
||||
|
||||
func (vs VSC) Value() (driver.Value, error) {
|
||||
fields := make([]string, len(vs))
|
||||
for i, v := range vs {
|
||||
fields[i] = strings.ReplaceAll(v, separator, "")
|
||||
}
|
||||
return strings.Join(fields, separator), nil
|
||||
}
|
||||
|
||||
var ErrWrongVSCType = errors.New("incompatible DB type for VSC")
|
||||
|
||||
func (vs *VSC) Scan(src any) error {
|
||||
var s string
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
s = v
|
||||
case []byte:
|
||||
s = string(v)
|
||||
case nil:
|
||||
*vs = nil
|
||||
return nil
|
||||
default:
|
||||
return ErrWrongVSCType
|
||||
}
|
||||
if s == "" {
|
||||
*vs = VSC{}
|
||||
return nil
|
||||
}
|
||||
*vs = strings.Split(s, separator)
|
||||
return nil
|
||||
}
|
||||
48
pkg/db/vsc_test.go
Normal file
48
pkg/db/vsc_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"
|
||||
)
|
||||
|
||||
func TestVSCDBRoundTrip(t *testing.T) {
|
||||
conn, err := sqlx.Open("sqlite3", ":memory:")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, conn.Close()) })
|
||||
|
||||
_, err = conn.Exec(`create table vsc_items (id integer primary key, items text not null)`)
|
||||
require.NoError(t, err)
|
||||
|
||||
type vscRow struct {
|
||||
ID int `db:"id"`
|
||||
Items db.VSC `db:"items"`
|
||||
}
|
||||
|
||||
for _, want := range []vscRow{
|
||||
{ID: 1, Items: db.VSC{"email"}},
|
||||
{ID: 2, Items: db.VSC{"email", "profile"}},
|
||||
{ID: 3, Items: db.VSC{}},
|
||||
} {
|
||||
_, err = conn.NamedExec(`insert into vsc_items (id, items) values (:id, :items)`, want)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got vscRow
|
||||
require.NoError(t, conn.Get(&got, `select id, items from vsc_items where id = ?`, want.ID))
|
||||
require.Equal(t, want, got)
|
||||
}
|
||||
t.Run("nil gets upgrade to empty list", func(t *testing.T) {
|
||||
want := vscRow{ID: 4, Items: nil}
|
||||
_, err = conn.NamedExec(`insert into vsc_items (id, items) values (:id, :items)`, want)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got vscRow
|
||||
require.NoError(t, conn.Get(&got, `select id, items from vsc_items where id = ?`, want.ID))
|
||||
require.NotNil(t, got.Items)
|
||||
require.Equal(t, db.VSC{}, got.Items)
|
||||
})
|
||||
}
|
||||
76
pkg/generic/mutexmap.go
Normal file
76
pkg/generic/mutexmap.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type MutexMap[K comparable, V any] struct {
|
||||
mu sync.RWMutex
|
||||
contents map[K]V
|
||||
}
|
||||
|
||||
func NewMutexMap[K comparable, V any]() *MutexMap[K, V] {
|
||||
return &MutexMap[K, V]{contents: make(map[K]V)}
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Set(key K, value V) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.contents[key] = value
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Get(key K) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
v, ok := m.contents[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Len() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.contents)
|
||||
}
|
||||
|
||||
// Upd atomically updates the value for key. `fn` is used to determine how to update an existing
|
||||
// value, if there is one. It will be passed the existing value, and should return `(newValue V, shouldReplace bool)`.
|
||||
//
|
||||
// Returns the value that ends up associated with key after the call and whether an update occurred.
|
||||
func (m *MutexMap[K, V]) Upd(key K, fn func(prev V) (next V, shouldReplace bool)) (V, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
prev := m.contents[key]
|
||||
if next, shouldReplace := fn(prev); shouldReplace {
|
||||
m.contents[key] = next
|
||||
return next, true
|
||||
}
|
||||
return prev, false
|
||||
}
|
||||
|
||||
// Del removes key from the map. Returns true if the key was present.
|
||||
func (m *MutexMap[K, V]) Del(key K) bool { //nolint: boolnames // Return value is incidental, not the main purpose of the function
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.contents[key]; !ok {
|
||||
return false
|
||||
}
|
||||
delete(m.contents, key)
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Cpy() map[K]V {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
cp := make(map[K]V, len(m.contents))
|
||||
maps.Copy(cp, m.contents)
|
||||
return cp
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Pop(key K) (ret V, isOk bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
ret, isOk = m.contents[key]
|
||||
delete(m.contents, key)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user