Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
151 changes: 151 additions & 0 deletions internal/filestore/channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package filestore

import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"sync"

"github.com/vim-jp/slacklog-generator/internal/store"
)

type channelStore struct {
dir string

rw sync.RWMutex
channels []store.Channel
idxID map[string]int
}

// Get gets a channel by ID.
func (cs *channelStore) Get(id string) (*store.Channel, error) {
err := cs.assureLoad()
if err != nil {
return nil, err
}
cs.rw.RLock()
defer cs.rw.RUnlock()

x, ok := cs.idxID[id]
if !ok {
return nil, fmt.Errorf("channel not found, uknown id: id=%s", id)
}
if x < 0 || x >= len(cs.channels) {
return nil, fmt.Errorf("channel index collapsed, ask developers: id=%s", id)
}
c := cs.channels[x]
return &c, nil
}

// Iterate enumerates all channels by callback.
// 呼び出し時点でチャンネル一覧のコピーが本イテレート専用に作成される。
// コールバックが false を返すと store.ErrIterateAbort が返る
func (cs *channelStore) Iterate(iter store.ChannelIterator) error {
err := cs.assureLoad()
if err != nil {
return err
}
cs.rw.RLock()
channels := make([]store.Channel, len(cs.channels))
copy(channels, cs.channels)
// FIXME: cs.idxID に入ってないのは省くべきでは?
cs.rw.RUnlock()
for i := range channels {
cont := iter.Iterate(&channels[i])
if !cont {
return store.ErrIterateAbort
}
}
return nil
}

// Upsert updates or inserts a channel in store.
// This returns true as 1st parameter, when a channel inserted.
func (cs *channelStore) Upsert(c store.Channel) (bool, error) {
if c.ID == "" {
return false, errors.New("empty ID is forbidden")
}

err := cs.assureLoad()
if err != nil {
return false, err
}
cs.rw.Lock()
defer cs.rw.Unlock()

c.Tidy()

x, ok := cs.idxID[c.ID]
if ok {
cs.channels[x] = c
return true, nil
}
cs.idxID[c.ID] = len(cs.channels)
cs.channels = append(cs.channels, c)
return false, nil
}

// Commit saves channels to file:channels.json.
func (cs *channelStore) Commit() error {
cs.rw.Lock()
defer cs.rw.Unlock()
if cs.channels == nil {
log.Printf("[DEBUG] no channels to commit. not load yet?")
return nil
}

ids := make([]string, 0, len(cs.idxID))
for id := range cs.idxID {
ids = append(ids, id)
}
sort.Strings(ids)
ca := make([]store.Channel, len(ids))
for i, id := range ids {
ca[i] = cs.channels[cs.idxID[id]]
}
err := jsonWriteFile(cs.path(), ca)
if err != nil {
return err
}

cs.replaceChannels(ca)
return nil
}

// path returns path for channels.json
func (cs *channelStore) path() string {
return filepath.Join(cs.dir, "channels.json")
}

// assureLoad assure channels.json is loaded.
func (cs *channelStore) assureLoad() error {
cs.rw.Lock()
defer cs.rw.Unlock()
if cs.channels != nil {
return nil
}
var channels []store.Channel
err := jsonReadFile(cs.path(), true, &channels)
if err != nil && !os.IsNotExist(err) {
return err
}
cs.replaceChannels(channels)
return nil
}

func (cs *channelStore) replaceChannels(channels []store.Channel) {
if len(channels) == 0 {
cs.channels = []store.Channel{}
cs.idxID = map[string]int{}
return
}
idxID := make(map[string]int, len(channels))
for i, c := range channels {
idxID[c.ID] = i
}
cs.channels = channels
cs.idxID = idxID
}
167 changes: 167 additions & 0 deletions internal/filestore/channel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package filestore

import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/vim-jp/slacklog-generator/internal/store"
"github.com/vim-jp/slacklog-generator/internal/testassert"
)

func jsonToChannel(t *testing.T, s string) *store.Channel {
t.Helper()
var c store.Channel
err := json.Unmarshal([]byte(s), &c)
if err != nil {
t.Fatalf("failed to parse as Channel: %s", err)
}
return &c
}

func TestChannelStore_Get(t *testing.T) {
cs := &channelStore{dir: "testdata/channel_read"}

for _, tc := range []struct {
id string
exp string
}{
{"CXXXX0001", `{"id":"CXXXX0001","name":"channel01"}`},
{"CXXXX0002", `{"id":"CXXXX0002","name":"channel02"}`},
{"CXXXX0003", `{"id":"CXXXX0003","name":"channel03"}`},
{"CXXXX0004", `{"id":"CXXXX0004","name":"channel04"}`},
{"CXXXX0005", `{"id":"CXXXX0005","name":"channel05"}`},
} {
act, err := cs.Get(tc.id)
if err != nil {
t.Fatalf("failed to get(%s): %s", tc.id, err)
}
exp := jsonToChannel(t, tc.exp)
testassert.Equal(t, exp, act, "id:"+tc.id)
}

c, err := cs.Get("CXXXX9999")
if err == nil {
t.Fatalf("should fail to get unknown ID: %+v", c)
}
if !strings.HasPrefix(err.Error(), "channel not found, ") {
t.Fatalf("unexpected error for getting unknown ID: %s", err)
}
}

func TestChannelStore_Iterate(t *testing.T) {
cs := &channelStore{dir: "testdata/channel_read"}

var act []*store.Channel
err := cs.Iterate(store.ChannelIterateFunc(func(c *store.Channel) bool {
act = append(act, c)
return true
}))
if err != nil {
t.Fatalf("iteration failed: %s", err)
}
exp := []*store.Channel{
jsonToChannel(t, `{"id":"CXXXX0001","name":"channel01"}`),
jsonToChannel(t, `{"id":"CXXXX0002","name":"channel02"}`),
jsonToChannel(t, `{"id":"CXXXX0003","name":"channel03"}`),
jsonToChannel(t, `{"id":"CXXXX0004","name":"channel04"}`),
jsonToChannel(t, `{"id":"CXXXX0005","name":"channel05"}`),
}
testassert.Equal(t, exp, act, "simple iteration")
}

func TestChannelStore_Iterate_Break(t *testing.T) {
cs := &channelStore{dir: "testdata/channel_read"}

i := 0
err := cs.Iterate(store.ChannelIterateFunc(func(_ *store.Channel) bool {
i++
return i > 2
}))
if !errors.Is(err, store.ErrIterateAbort) {
t.Fatalf("iterate should be failed with:%s got:%s", store.ErrIterateAbort, err)
}
}

func TestChannelStore_Write(t *testing.T) {
dir, err := ioutil.TempDir("testdata", "channel_write*")
if err != nil {
t.Fatalf("failed to TempDir: %s", err)
}
t.Cleanup(func() {
os.RemoveAll(dir)
})
cs := &channelStore{dir: dir}

for i, s := range []string{
`{"id":"W0001","name":"channel01"}`,
`{"id":"W0009","name":"channel09"}`,
`{"id":"W0005","name":"channel05"}`,
`{"id":"W0001","name":"channel01a"}`,
} {
_, err := cs.Upsert(*jsonToChannel(t, s))
if err != nil {
t.Fatalf("upsert failed #%d: %s", i, err)
}
}
err = cs.Commit()
if err != nil {
t.Fatalf("commit failed: %s", err)
}

cs2 := &channelStore{dir: dir}
err = cs2.assureLoad()
if err != nil {
t.Fatalf("assureLoad failed: %s", err)
}
testassert.Equal(t, []store.Channel{
*jsonToChannel(t, `{"id":"W0001","name":"channel01a"}`),
*jsonToChannel(t, `{"id":"W0005","name":"channel05"}`),
*jsonToChannel(t, `{"id":"W0009","name":"channel09"}`),
}, cs2.channels, "wrote channels.json")
}

func TestChannelStore_Upsert_NoID(t *testing.T) {
dir, err := ioutil.TempDir("testdata", "channel_write*")
if err != nil {
t.Fatalf("failed to TempDir: %s", err)
}
t.Cleanup(func() {
os.RemoveAll(dir)
})
cs := &channelStore{dir: dir}
_, err = cs.Upsert(*jsonToChannel(t, `{"name":"foobar"}`))
if err == nil {
t.Fatal("upsert without ID should be failed")
}
if err.Error() != "empty ID is forbidden" {
t.Fatalf("unexpected failure: %s", err)
}
}

func TestChannelStore_Commit_Empty(t *testing.T) {
// 空のCommitは channels.json を作らない
dir, err := ioutil.TempDir("testdata", "channel_write*")
if err != nil {
t.Fatalf("failed to TempDir: %s", err)
}
t.Cleanup(func() {
os.RemoveAll(dir)
})
cs := &channelStore{dir: dir}

err = cs.Commit()
if err != nil {
t.Fatalf("unexpted failure: %s", err)
}
fi, err := os.Stat(cs.path())
if err == nil {
t.Fatalf("channels.json created unexpectedly: %s", fi.Name())
}
if !os.IsNotExist(err) {
t.Fatalf("unexpected failure: %s", err)
}
}
Loading