Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c683e87ab | ||
|
|
c41d13bb77 | ||
|
|
69af7eaee3 | ||
|
|
f97e9b0aa9 | ||
|
|
92cb9267fc | ||
|
|
4de3218c40 |
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
package exporter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/abelith/spellbook/internal/models"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Exporter interface {
|
||||||
|
Export(ctx context.Context, ch <-chan models.Spell, w io.Writer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
exporters = make(map[string]Exporter)
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(name string, exp Exporter) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if exp == nil {
|
||||||
|
panic("exporter: Exporter cannot be nil")
|
||||||
|
}
|
||||||
|
if _, ok := exporters[name]; ok {
|
||||||
|
panic("exporter: Register called twice for " + name)
|
||||||
|
}
|
||||||
|
exporters[name] = exp
|
||||||
|
}
|
||||||
|
|
||||||
|
func Export(ctx context.Context, name string, ch <-chan models.Spell, w io.Writer) error {
|
||||||
|
mu.Lock()
|
||||||
|
exp, ok := exporters[name]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("exporter %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exp.Export(ctx, ch, w)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package importer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/abelith/spellbook/internal/models"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Importer interface {
|
||||||
|
Import(ctx context.Context, r io.Reader) (<-chan models.Spell, <-chan error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
importers = make(map[string]Importer)
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(name string, imp Importer) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if imp == nil {
|
||||||
|
panic("importer: Importer cannot be nil")
|
||||||
|
}
|
||||||
|
if _, ok := importers[name]; ok {
|
||||||
|
panic("importer: Register called twice for " + name)
|
||||||
|
}
|
||||||
|
importers[name] = imp
|
||||||
|
}
|
||||||
|
|
||||||
|
func Import(ctx context.Context, name string, r io.Reader) (<-chan models.Spell, <-chan error, error) {
|
||||||
|
mu.Lock()
|
||||||
|
imp, ok := importers[name]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("importer %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imp.Import(ctx, r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/abelith/spellbook/internal/adapters/exporter"
|
||||||
|
"github.com/abelith/spellbook/internal/adapters/importer"
|
||||||
|
"github.com/abelith/spellbook/internal/models"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
exporter.Register("jsonl", &JsonlExporter{})
|
||||||
|
importer.Register("jsonl", &JsonlImporter{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonlExporter struct{}
|
||||||
|
|
||||||
|
type JsonlImporter struct{}
|
||||||
|
|
||||||
|
func (je *JsonlExporter) Export(ctx context.Context, ch <-chan models.Spell, w io.Writer) error {
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case spell, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Encode(&spell); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ji JsonlImporter) Import(ctx context.Context, r io.Reader) (<-chan models.Spell, <-chan error, error) {
|
||||||
|
ch := make(chan models.Spell, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
dec := json.NewDecoder(r)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer close(errCh)
|
||||||
|
|
||||||
|
for dec.More() {
|
||||||
|
var spell models.Spell
|
||||||
|
if err := dec.Decode(&spell); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
errCh <- ctx.Err()
|
||||||
|
return
|
||||||
|
case ch <- spell:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh, nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
"github.com/abelith/spellbook/internal/errors"
|
"github.com/abelith/spellbook/internal/errors"
|
||||||
"github.com/abelith/spellbook/internal/models"
|
"github.com/abelith/spellbook/internal/models"
|
||||||
@@ -45,17 +46,10 @@ func (r *Repo) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) Add(ctx context.Context, spell models.Spell) error {
|
func (r *Repo) Add(ctx context.Context, spell models.Spell) error {
|
||||||
tags := strings.Join(spell.Tags, ",")
|
if _, err := insertSpell(ctx, r.db, spell); err != nil {
|
||||||
|
|
||||||
_, err := sq.Insert("spells").
|
|
||||||
Columns("name", "spell", "short_desc", "description", "tags").
|
|
||||||
Values(spell.Name, spell.Spell, spell.ShortDesc, spell.Description, tags).
|
|
||||||
PlaceholderFormat(sq.Question).
|
|
||||||
RunWith(r.db).
|
|
||||||
ExecContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +142,97 @@ func (r *Repo) GetMany(ctx context.Context, query models.SearchQuery) ([]models.
|
|||||||
return spells, nil
|
return spells, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Import(ctx context.Context, ch <-chan models.Spell) error {
|
||||||
|
return r.runTx(ctx, func(tx *sql.Tx) error {
|
||||||
|
cache := sq.NewStmtCache(tx)
|
||||||
|
defer cache.Clear()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case spell, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := insertSpell(ctx, cache, spell)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Count(ctx context.Context) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := sq.Select("COUNT(*)").
|
||||||
|
From("spells").
|
||||||
|
RunWith(r.db).
|
||||||
|
ScanContext(ctx, &count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Export(ctx context.Context) (<-chan models.Spell, <-chan error, error) {
|
||||||
|
ch := make(chan models.Spell, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
rows, err := sq.Select("id", "name", "spell", "short_desc", "description", "tags").
|
||||||
|
From("spells").
|
||||||
|
RunWith(r.db).
|
||||||
|
QueryContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(errCh)
|
||||||
|
defer close(ch)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
spell, err := scanSpell(rows)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
errCh <- ctx.Err()
|
||||||
|
return
|
||||||
|
case ch <- spell:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) runTx(ctx context.Context, f func(tx *sql.Tx) error) error {
|
||||||
|
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to begin tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f(tx); err != nil {
|
||||||
|
if rbErr := tx.Rollback(); rbErr != nil {
|
||||||
|
return fmt.Errorf("err: %v, rollback err: %w", err, rbErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
func scanSpell(row scanner) (models.Spell, error) {
|
func scanSpell(row scanner) (models.Spell, error) {
|
||||||
spell := models.Spell{}
|
spell := models.Spell{}
|
||||||
tagsString := ""
|
tagsString := ""
|
||||||
@@ -162,3 +247,14 @@ func scanSpell(row scanner) (models.Spell, error) {
|
|||||||
|
|
||||||
return spell, nil
|
return spell, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insertSpell(ctx context.Context, runner sq.BaseRunner, spell models.Spell) (sql.Result, error) {
|
||||||
|
tags := strings.Join(spell.Tags, ",")
|
||||||
|
|
||||||
|
return sq.Insert("spells").
|
||||||
|
Columns("name", "spell", "short_desc", "description", "tags").
|
||||||
|
Values(spell.Name, spell.Spell, spell.ShortDesc, spell.Description, tags).
|
||||||
|
PlaceholderFormat(sq.Question).
|
||||||
|
RunWith(runner).
|
||||||
|
ExecContext(ctx)
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/abelith/spellbook/internal/adapters/exporter"
|
||||||
|
"github.com/abelith/spellbook/internal/adapters/importer"
|
||||||
"github.com/abelith/spellbook/internal/models"
|
"github.com/abelith/spellbook/internal/models"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SpellCaster interface {
|
type SpellCaster interface {
|
||||||
@@ -12,11 +15,14 @@ type SpellCaster interface {
|
|||||||
type SpellCommand interface {
|
type SpellCommand interface {
|
||||||
Add(ctx context.Context, spell models.Spell) error
|
Add(ctx context.Context, spell models.Spell) error
|
||||||
Delete(ctx context.Context, spellName string) error
|
Delete(ctx context.Context, spellName string) error
|
||||||
|
Import(ctx context.Context, ch <-chan models.Spell) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpellQuery interface {
|
type SpellQuery interface {
|
||||||
Get(ctx context.Context, spellName string) (models.Spell, error)
|
Get(ctx context.Context, spellName string) (models.Spell, error)
|
||||||
GetMany(ctx context.Context, query models.SearchQuery) ([]models.Spell, error)
|
GetMany(ctx context.Context, query models.SearchQuery) ([]models.Spell, error)
|
||||||
|
Count(ctx context.Context) (int, error)
|
||||||
|
Export(ctx context.Context) (<-chan models.Spell, <-chan error, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
@@ -67,3 +73,50 @@ func (a *Application) CastSpell(
|
|||||||
) (models.CastResult, error) {
|
) (models.CastResult, error) {
|
||||||
return a.caster.Cast(ctx, spellCast)
|
return a.caster.Cast(ctx, spellCast)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Application) ExportSpells(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
w io.Writer,
|
||||||
|
) error {
|
||||||
|
ch, errCh, err := a.spellQuery.Export(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exporter.Export(ctx, name, ch, w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Application) ImportSpells(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
r io.Reader,
|
||||||
|
) error {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ch, errCh, err := importer.Import(ctx, name, r)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.spellCmd.Import(ctx, ch); err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ var reservedCommands = map[string]struct{}{
|
|||||||
"help": {},
|
"help": {},
|
||||||
"completion": {},
|
"completion": {},
|
||||||
"search": {},
|
"search": {},
|
||||||
|
"export": {},
|
||||||
|
"import": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAddCommand(app Application) *cobra.Command {
|
func NewAddCommand(app Application) *cobra.Command {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"github.com/abelith/spellbook/internal/models"
|
"github.com/abelith/spellbook/internal/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Application interface {
|
type Application interface {
|
||||||
@@ -12,6 +13,8 @@ type Application interface {
|
|||||||
GetSpells(ctx context.Context, query models.SearchQuery) ([]models.Spell, error)
|
GetSpells(ctx context.Context, query models.SearchQuery) ([]models.Spell, error)
|
||||||
DeleteSpell(ctx context.Context, spellName string) error
|
DeleteSpell(ctx context.Context, spellName string) error
|
||||||
CastSpell(ctx context.Context, spellCast models.SpellCast) (models.CastResult, error)
|
CastSpell(ctx context.Context, spellCast models.SpellCast) (models.CastResult, error)
|
||||||
|
ExportSpells(ctx context.Context, exp string, w io.Writer) error
|
||||||
|
ImportSpells(ctx context.Context, exp string, r io.Reader) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
@@ -24,6 +27,8 @@ func NewCLI(ver string, app Application) *CLI {
|
|||||||
NewAddCommand(app),
|
NewAddCommand(app),
|
||||||
NewRmCommand(app),
|
NewRmCommand(app),
|
||||||
NewSearchCmd(app),
|
NewSearchCmd(app),
|
||||||
|
NewExportCmd(app),
|
||||||
|
NewImportCmd(app),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &CLI{
|
return &CLI{
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
_ "github.com/abelith/spellbook/internal/adapters/json"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewExportCmd(app Application) *cobra.Command {
|
||||||
|
var format string
|
||||||
|
var out string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "export",
|
||||||
|
Short: "Export spellbook",
|
||||||
|
Long: `WIP`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
var w io.Writer = os.Stdout
|
||||||
|
if out != "" {
|
||||||
|
f, err := os.OpenFile(out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if innerErr := f.Close(); innerErr != nil {
|
||||||
|
err = errors.Join(err, innerErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
w = f
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufio.NewWriter(w)
|
||||||
|
defer func() {
|
||||||
|
if innerErr := buf.Flush(); innerErr != nil {
|
||||||
|
err = errors.Join(err, innerErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return app.ExportSpells(ctx, format, buf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&format, "format", "f", "jsonl", "Output format")
|
||||||
|
cmd.Flags().StringVarP(&out, "out", "o", "", "Output")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
_ "github.com/abelith/spellbook/internal/adapters/json"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewImportCmd(app Application) *cobra.Command {
|
||||||
|
var format string
|
||||||
|
var in string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "import",
|
||||||
|
Short: "Import spellbook",
|
||||||
|
Long: `WIP`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
var r io.Reader = os.Stdin
|
||||||
|
if in != "" {
|
||||||
|
f, err := os.Open(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if innerErr := f.Close(); innerErr != nil {
|
||||||
|
err = errors.Join(err, innerErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
r = f
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufio.NewReader(r)
|
||||||
|
return app.ImportSpells(ctx, format, buf)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVarP(&format, "format", "f", "jsonl", "Input format")
|
||||||
|
cmd.Flags().StringVarP(&in, "input", "i", "", "Input")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Spell struct {
|
type Spell struct {
|
||||||
Id int
|
Id int `json:"-"`
|
||||||
Name string
|
Name string `json:"name,omitempty"`
|
||||||
Spell string
|
Spell string `json:"spell,omitempty"`
|
||||||
ShortDesc string
|
ShortDesc string `json:"short_desc,omitempty"`
|
||||||
Description string
|
Description string `json:"description,omitempty"`
|
||||||
Tags []string
|
Tags []string `json:"tags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpellCast struct {
|
type SpellCast struct {
|
||||||
|
|||||||
Reference in new issue
Block a user