Merge pull request 'exporter' (#46) from exporter into trunk
Reviewed-on: #46
This commit is contained in:
commit
043974b074
18 changed files with 495 additions and 183 deletions
|
@ -44,3 +44,12 @@ func PtrOrNull[T comparable](val T) *T {
|
|||
|
||||
return &val
|
||||
}
|
||||
|
||||
func ZeroOr[T any](v *T) T {
|
||||
var zero T
|
||||
if v == nil {
|
||||
return zero
|
||||
}
|
||||
|
||||
return *v
|
||||
}
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
package calls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/pb"
|
||||
|
||||
tgs "dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
)
|
||||
|
||||
type TalkgroupFilter struct {
|
||||
Talkgroups []tgs.ID `json:"talkgroups,omitempty"`
|
||||
TalkgroupsNot []tgs.ID `json:"talkgroupsNot,omitempty"`
|
||||
TalkgroupTagsAll []string `json:"talkgroupTagsAll,omitempty"`
|
||||
TalkgroupTagsAny []string `json:"talkgroupTagsAny,omitempty"`
|
||||
TalkgroupTagsNot []string `json:"talkgroupTagsNot,omitempty"`
|
||||
|
||||
talkgroups map[tgs.ID]bool
|
||||
}
|
||||
|
||||
func TalkgroupFilterFromPB(ctx context.Context, p *pb.Filter) (*TalkgroupFilter, error) {
|
||||
tgf := &TalkgroupFilter{
|
||||
TalkgroupTagsAll: p.TalkgroupTagsAll,
|
||||
TalkgroupTagsAny: p.TalkgroupTagsAny,
|
||||
TalkgroupTagsNot: p.TalkgroupTagsNot,
|
||||
}
|
||||
|
||||
if l := len(p.Talkgroups); l > 0 {
|
||||
tgf.Talkgroups = make([]tgs.ID, l)
|
||||
for i, t := range p.Talkgroups {
|
||||
tgf.Talkgroups[i] = tgs.ID{
|
||||
System: uint32(t.System),
|
||||
Talkgroup: uint32(t.Talkgroup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(p.TalkgroupsNot); l > 0 {
|
||||
tgf.TalkgroupsNot = make([]tgs.ID, l)
|
||||
for i, t := range p.TalkgroupsNot {
|
||||
tgf.TalkgroupsNot[i] = tgs.ID{
|
||||
System: uint32(t.System),
|
||||
Talkgroup: uint32(t.Talkgroup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tgf, tgf.compile(ctx)
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) hasTags() bool {
|
||||
return len(f.TalkgroupTagsAny) > 0 || len(f.TalkgroupTagsAll) > 0 || len(f.TalkgroupTagsNot) > 0
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) GetFinalTalkgroups() map[tgs.ID]bool {
|
||||
return f.talkgroups
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) compile(ctx context.Context) error {
|
||||
f.talkgroups = make(map[tgs.ID]bool)
|
||||
for _, tg := range f.Talkgroups {
|
||||
f.talkgroups[tg] = true
|
||||
}
|
||||
|
||||
if f.hasTags() { // don't bother with DB if no tags
|
||||
db := database.FromCtx(ctx)
|
||||
tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tg := range tagTGs {
|
||||
f.talkgroups[tgs.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.TGID)}] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, tg := range f.TalkgroupsNot {
|
||||
f.talkgroups[tg] = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) Test(ctx context.Context, call *Call) bool {
|
||||
if f == nil { // no filter means all calls
|
||||
return true
|
||||
}
|
||||
|
||||
if f.talkgroups == nil {
|
||||
err := f.compile(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
tg := call.TalkgroupTuple()
|
||||
|
||||
tgRes, have := f.talkgroups[tg]
|
||||
if have {
|
||||
return tgRes
|
||||
}
|
||||
|
||||
for _, patch := range call.Patches {
|
||||
tg.Talkgroup = uint32(patch)
|
||||
tgRes, have := f.talkgroups[tg]
|
||||
if have {
|
||||
return tgRes
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -8,9 +8,9 @@ import (
|
|||
"sync"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/version"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/pb"
|
||||
tgfilter "dynatron.me/x/stillbox/pkg/talkgroups/filter"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
@ -33,7 +33,7 @@ type client struct {
|
|||
Connection
|
||||
|
||||
liveState pb.LiveState
|
||||
filter *calls.TalkgroupFilter
|
||||
filter *tgfilter.TalkgroupFilter
|
||||
|
||||
nexus *Nexus
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package nexus
|
|||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/pb"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
tgfilter "dynatron.me/x/stillbox/pkg/talkgroups/filter"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -111,7 +111,7 @@ func (c *client) Live(ctx context.Context, cmd *pb.Live) error {
|
|||
}
|
||||
|
||||
if cmd.Filter != nil {
|
||||
filter, err := calls.TalkgroupFilterFromPB(ctx, cmd.Filter)
|
||||
filter, err := tgfilter.TalkgroupFilterFromPB(ctx, cmd.Filter)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("filter create failed")
|
||||
return err
|
||||
|
|
|
@ -75,7 +75,6 @@ func errTextNotFound(err error) render.Renderer {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func internalError(err error) render.Renderer {
|
||||
return &errResponse{
|
||||
Err: err,
|
||||
|
@ -89,7 +88,7 @@ type errResponder func(error) render.Renderer
|
|||
var statusMapping = map[error]errResponder{
|
||||
tgstore.ErrNoSuchSystem: errTextNotFound,
|
||||
tgstore.ErrNotFound: errTextNotFound,
|
||||
pgx.ErrNoRows: recordNotFound,
|
||||
pgx.ErrNoRows: recordNotFound,
|
||||
}
|
||||
|
||||
func autoError(err error) render.Renderer {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/importer"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
@ -114,7 +114,7 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) {
|
||||
var impJob importer.ImportJob
|
||||
var impJob xport.ImportJob
|
||||
err := forms.Unmarshal(r, &impJob, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
)
|
||||
|
||||
type DatabaseSink struct {
|
||||
db database.Store
|
||||
db database.Store
|
||||
tgs tgstore.Store
|
||||
}
|
||||
|
||||
|
|
168
pkg/talkgroups/filter/filter.go
Normal file
168
pkg/talkgroups/filter/filter.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/pb"
|
||||
|
||||
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
)
|
||||
|
||||
type TalkgroupFilter struct {
|
||||
Talkgroups []tgsp.ID `json:"talkgroups,omitempty"`
|
||||
TalkgroupsNot []tgsp.ID `json:"talkgroupsNot,omitempty"`
|
||||
TalkgroupTagsAll []string `json:"talkgroupTagsAll,omitempty"`
|
||||
TalkgroupTagsAny []string `json:"talkgroupTagsAny,omitempty"`
|
||||
TalkgroupTagsNot []string `json:"talkgroupTagsNot,omitempty"`
|
||||
|
||||
talkgroups map[tgsp.ID]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) TGs(ctx context.Context) (tgsp.IDs, error) {
|
||||
err := f.ensureCompiled(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := make(tgsp.IDs, 0, len(f.talkgroups))
|
||||
for tg := range f.talkgroups {
|
||||
r = append(r, tg)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) Tuples(ctx context.Context) (database.TGTuples, error) {
|
||||
err := f.ensureCompiled(ctx)
|
||||
if err != nil {
|
||||
return database.TGTuples{}, err
|
||||
}
|
||||
|
||||
sys := make([]uint32, len(f.talkgroups))
|
||||
tgs := make([]uint32, len(f.talkgroups))
|
||||
|
||||
i := 0
|
||||
for tg := range f.talkgroups {
|
||||
sys[i] = tg.System
|
||||
tgs[i] = tg.Talkgroup
|
||||
}
|
||||
|
||||
return database.TGTuples{sys, tgs}, nil
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) ensureCompiled(ctx context.Context) error {
|
||||
if f.talkgroups == nil {
|
||||
return f.compile(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tgf *TalkgroupFilter) IsEmpty() bool {
|
||||
if tgf == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(tgf.Talkgroups) > 0 ||
|
||||
len(tgf.TalkgroupsNot) > 0 ||
|
||||
len(tgf.TalkgroupTagsAll) > 0 ||
|
||||
len(tgf.TalkgroupTagsAny) > 0 ||
|
||||
len(tgf.TalkgroupsNot) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TalkgroupFilterFromPB(ctx context.Context, p *pb.Filter) (*TalkgroupFilter, error) {
|
||||
tgf := &TalkgroupFilter{
|
||||
TalkgroupTagsAll: p.TalkgroupTagsAll,
|
||||
TalkgroupTagsAny: p.TalkgroupTagsAny,
|
||||
TalkgroupTagsNot: p.TalkgroupTagsNot,
|
||||
}
|
||||
|
||||
if l := len(p.Talkgroups); l > 0 {
|
||||
tgf.Talkgroups = make([]tgsp.ID, l)
|
||||
for i, t := range p.Talkgroups {
|
||||
tgf.Talkgroups[i] = tgsp.ID{
|
||||
System: uint32(t.System),
|
||||
Talkgroup: uint32(t.Talkgroup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(p.TalkgroupsNot); l > 0 {
|
||||
tgf.TalkgroupsNot = make([]tgsp.ID, l)
|
||||
for i, t := range p.TalkgroupsNot {
|
||||
tgf.TalkgroupsNot[i] = tgsp.ID{
|
||||
System: uint32(t.System),
|
||||
Talkgroup: uint32(t.Talkgroup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tgf, tgf.compile(ctx)
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) hasTags() bool {
|
||||
return len(f.TalkgroupTagsAny) > 0 || len(f.TalkgroupTagsAll) > 0 || len(f.TalkgroupTagsNot) > 0
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) GetFinalTalkgroups() map[tgsp.ID]bool {
|
||||
return f.talkgroups
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) compile(ctx context.Context) error {
|
||||
f.talkgroups = make(map[tgsp.ID]bool)
|
||||
for _, tg := range f.Talkgroups {
|
||||
f.talkgroups[tg] = true
|
||||
}
|
||||
|
||||
if f.hasTags() { // don't bother with DB if no tags
|
||||
db := database.FromCtx(ctx)
|
||||
tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tg := range tagTGs {
|
||||
f.talkgroups[tgsp.ID{System: uint32(tg.SystemID), Talkgroup: uint32(tg.TGID)}] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, tg := range f.TalkgroupsNot {
|
||||
f.talkgroups[tg] = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *TalkgroupFilter) Test(ctx context.Context, call *calls.Call) bool {
|
||||
if f == nil { // no filter means all calls
|
||||
return true
|
||||
}
|
||||
|
||||
err := f.ensureCompiled(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tg := call.TalkgroupTuple()
|
||||
|
||||
tgRes, have := f.talkgroups[tg]
|
||||
if have {
|
||||
return tgRes
|
||||
}
|
||||
|
||||
for _, patch := range call.Patches {
|
||||
tg.Talkgroup = uint32(patch)
|
||||
tgRes, have := f.talkgroups[tg]
|
||||
if have {
|
||||
return tgRes
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -9,9 +9,9 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
@ -135,6 +135,15 @@ func (t *cache) Hint(ctx context.Context, tgs []tgsp.ID) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (t *cache) get(id tgsp.ID) (*tgsp.Talkgroup, bool) {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
|
||||
tg, has := t.tgs[id]
|
||||
|
||||
return tg, has
|
||||
}
|
||||
|
||||
func (t *cache) add(rec *tgsp.Talkgroup) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
@ -176,16 +185,14 @@ func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs) ([]*tgsp.Talkgroup, error
|
|||
var err error
|
||||
if tgs != nil {
|
||||
toGet := make(tgsp.IDs, 0, len(tgs))
|
||||
t.RLock()
|
||||
for _, id := range tgs {
|
||||
rec, has := t.tgs[id]
|
||||
rec, has := t.get(id)
|
||||
if has {
|
||||
r = append(r, rec)
|
||||
} else {
|
||||
toGet = append(toGet, id)
|
||||
}
|
||||
}
|
||||
t.RUnlock()
|
||||
|
||||
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples())
|
||||
if err != nil {
|
||||
|
@ -240,9 +247,7 @@ func (t *cache) SystemTGs(ctx context.Context, systemID int32) ([]*tgsp.Talkgrou
|
|||
}
|
||||
|
||||
func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) {
|
||||
t.RLock()
|
||||
rec, has := t.tgs[tg]
|
||||
t.RUnlock()
|
||||
rec, has := t.get(tg)
|
||||
|
||||
if has {
|
||||
return rec, nil
|
||||
|
@ -326,7 +331,7 @@ func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, er
|
|||
tg := &tgsp.Talkgroup{
|
||||
Talkgroup: tgm,
|
||||
System: database.System{
|
||||
ID: c.System,
|
||||
ID: c.System,
|
||||
Name: sys,
|
||||
},
|
||||
Learned: tgm.Learned,
|
||||
|
@ -361,7 +366,6 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
|||
input[i].SystemID = int32(system)
|
||||
input[i].Learned = common.PtrTo(false)
|
||||
|
||||
|
||||
}
|
||||
|
||||
var oerr error
|
||||
|
@ -375,8 +379,8 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
|||
return
|
||||
}
|
||||
versionParams = append(versionParams, database.StoreTGVersionParams{
|
||||
SystemID: int32(system),
|
||||
TGID: r.TGID,
|
||||
SystemID: int32(system),
|
||||
TGID: r.TGID,
|
||||
Submitter: auth.UIDFrom(ctx),
|
||||
})
|
||||
tgs = append(tgs, &tgsp.Talkgroup{
|
||||
|
|
54
pkg/talkgroups/xport/export.go
Normal file
54
pkg/talkgroups/xport/export.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package xport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/filter"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport/sdrtrunk"
|
||||
)
|
||||
|
||||
type Exporter interface {
|
||||
ExportTalkgroups(ctx context.Context, w io.Writer, tgs []*talkgroups.Talkgroup, tmpl []byte) error
|
||||
}
|
||||
|
||||
type ExportJob struct {
|
||||
Type Format `json:"type"`
|
||||
SystemID int `json:"systemID"`
|
||||
Template []byte `json:"template"`
|
||||
|
||||
filter.TalkgroupFilter
|
||||
Exporter
|
||||
}
|
||||
|
||||
func (ej *ExportJob) Export(ctx context.Context, w io.Writer) error {
|
||||
var tgs []*talkgroups.Talkgroup
|
||||
var err error
|
||||
tgst := tgstore.FromCtx(ctx)
|
||||
if ej.TalkgroupFilter.IsEmpty() {
|
||||
tgs, err = tgst.SystemTGs(ctx, int32(ej.SystemID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ids, err := ej.TalkgroupFilter.TGs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tgs, err = tgst.TGs(ctx, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch ej.Type {
|
||||
case FormatSDRTrunk:
|
||||
ej.Exporter = sdrtrunk.New()
|
||||
default:
|
||||
return ErrBadType
|
||||
}
|
||||
|
||||
return ej.ExportTalkgroups(ctx, w, tgs, ej.Template)
|
||||
}
|
16
pkg/talkgroups/xport/format.go
Normal file
16
pkg/talkgroups/xport/format.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package xport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatRadioReference Format = "radioreference"
|
||||
FormatSDRTrunk Format = "sdrtrunk"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadType = errors.New("unknown format type")
|
||||
)
|
35
pkg/talkgroups/xport/import.go
Normal file
35
pkg/talkgroups/xport/import.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package xport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport/radioref"
|
||||
)
|
||||
|
||||
type Importer interface {
|
||||
ImportTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error)
|
||||
}
|
||||
|
||||
type ImportJob struct {
|
||||
Type Format `json:"type"`
|
||||
SystemID int `json:"systemID"`
|
||||
Body string `json:"body"`
|
||||
|
||||
Importer `json:"-"`
|
||||
}
|
||||
|
||||
func (ij *ImportJob) Import(ctx context.Context) ([]talkgroups.Talkgroup, error) {
|
||||
r := bytes.NewReader([]byte(ij.Body))
|
||||
|
||||
switch ij.Type {
|
||||
case FormatRadioReference:
|
||||
ij.Importer = radioref.New()
|
||||
default:
|
||||
return nil, ErrBadType
|
||||
}
|
||||
|
||||
return ij.ImportTalkgroups(ctx, ij.SystemID, r)
|
||||
}
|
51
pkg/talkgroups/xport/import_test.go
Normal file
51
pkg/talkgroups/xport/import_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package xport_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestImport(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
impType string
|
||||
input []byte
|
||||
sysID int
|
||||
sysName string
|
||||
jsExpect []byte
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "unknown importer",
|
||||
impType: "nonexistent",
|
||||
expectErr: xport.ErrBadType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ij := &xport.ImportJob{
|
||||
Type: xport.Format(tc.impType),
|
||||
SystemID: tc.sysID,
|
||||
Body: string(tc.input),
|
||||
}
|
||||
|
||||
_, err := ij.Import(ctx)
|
||||
|
||||
if tc.expectErr != nil {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectErr.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
package importer
|
||||
package radioref
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
@ -16,42 +14,10 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
)
|
||||
|
||||
type ImportSource string
|
||||
type Driver struct{}
|
||||
|
||||
const (
|
||||
ImportSrcRadioReference ImportSource = "radioreference"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadImportType = errors.New("unknown import type")
|
||||
)
|
||||
|
||||
type importer interface {
|
||||
importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error)
|
||||
}
|
||||
|
||||
type ImportJob struct {
|
||||
Type ImportSource `json:"type"`
|
||||
SystemID int `json:"systemID"`
|
||||
Body string `json:"body"`
|
||||
|
||||
importer `json:"-"`
|
||||
}
|
||||
|
||||
func (ij *ImportJob) Import(ctx context.Context) ([]talkgroups.Talkgroup, error) {
|
||||
r := bytes.NewReader([]byte(ij.Body))
|
||||
|
||||
switch ij.Type {
|
||||
case ImportSrcRadioReference:
|
||||
ij.importer = new(radioReferenceImporter)
|
||||
default:
|
||||
return nil, ErrBadImportType
|
||||
}
|
||||
|
||||
return ij.importTalkgroups(ctx, ij.SystemID, r)
|
||||
}
|
||||
|
||||
type radioReferenceImporter struct {
|
||||
func New() *Driver {
|
||||
return new(Driver)
|
||||
}
|
||||
|
||||
type rrState int
|
||||
|
@ -64,7 +30,7 @@ const (
|
|||
|
||||
var rrRE = regexp.MustCompile(`DEC\s+HEX\s+Mode\s+Alpha Tag\s+Description\s+Tag`)
|
||||
|
||||
func (rr *radioReferenceImporter) importTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) {
|
||||
func (rr *Driver) ImportTalkgroups(ctx context.Context, sys int, r io.Reader) ([]talkgroups.Talkgroup, error) {
|
||||
sc := bufio.NewScanner(r)
|
||||
tgs := make([]talkgroups.Talkgroup, 0, 8)
|
||||
sysn, has := tgstore.FromCtx(ctx).SystemName(ctx, sys)
|
|
@ -1,4 +1,4 @@
|
|||
package importer_test
|
||||
package radioref_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -15,8 +15,8 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/database/mocks"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/importer"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
)
|
||||
|
||||
func getFixture(fixture string) []byte {
|
||||
|
@ -28,7 +28,7 @@ func getFixture(fixture string) []byte {
|
|||
return fixt
|
||||
}
|
||||
|
||||
func TestImport(t *testing.T) {
|
||||
func TestRadioRef(t *testing.T) {
|
||||
// this is for deterministic UUIDs
|
||||
uuid.SetRand(rand.New(rand.NewSource(1)))
|
||||
|
||||
|
@ -42,18 +42,13 @@ func TestImport(t *testing.T) {
|
|||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "radioreference",
|
||||
name: "radioreference import",
|
||||
impType: "radioreference",
|
||||
input: getFixture("riscon.txt"),
|
||||
jsExpect: getFixture("riscon.json"),
|
||||
sysID: 197,
|
||||
sysName: "RISCON",
|
||||
},
|
||||
{
|
||||
name: "unknown importer",
|
||||
impType: "nonexistent",
|
||||
expectErr: importer.ErrBadImportType,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
@ -64,8 +59,8 @@ func TestImport(t *testing.T) {
|
|||
}
|
||||
ctx := database.CtxWithDB(context.Background(), dbMock)
|
||||
ctx = tgstore.CtxWithStore(ctx, tgstore.NewCache())
|
||||
ij := &importer.ImportJob{
|
||||
Type: importer.ImportSource(tc.impType),
|
||||
ij := &xport.ImportJob{
|
||||
Type: xport.Format(tc.impType),
|
||||
SystemID: tc.sysID,
|
||||
Body: string(tc.input),
|
||||
}
|
128
pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go
Normal file
128
pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package sdrtrunk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
)
|
||||
|
||||
type Playlist struct {
|
||||
XMLName xml.Name `xml:"playlist"`
|
||||
Version int `xml:"version,attr"`
|
||||
Aliases []Alias `xml:"alias"`
|
||||
Channels []Channel `xml:"channel,omitempty"`
|
||||
Streams []Stream `xml:"stream,omitempty"`
|
||||
}
|
||||
|
||||
type Alias struct {
|
||||
XMLName xml.Name `xml:"alias"`
|
||||
Name string `xml:"name,attr,omitempty"`
|
||||
Color int `xml:"color,attr,omitempty"`
|
||||
Group string `xml:"group,attr,omitempty"`
|
||||
IconName string `xml:"iconName,attr,omitempty"`
|
||||
List string `xml:"list,attr,omitempty"`
|
||||
IDs []ID `xml:"id"`
|
||||
}
|
||||
|
||||
func tgToAlias(tg *talkgroups.Talkgroup) Alias {
|
||||
return Alias{
|
||||
XMLName: xml.Name{Local: "alias"},
|
||||
Name: common.ZeroOr(tg.Name),
|
||||
Group: common.ZeroOr(tg.TGGroup),
|
||||
List: "Stillbox",
|
||||
IDs: []ID{
|
||||
ID{
|
||||
XMLName: xml.Name{Local: "id"},
|
||||
Type: "talkgroup",
|
||||
Value: common.PtrTo(int(tg.TGID)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type ID struct {
|
||||
XMLName xml.Name `xml:"id"`
|
||||
Type string `xml:"type,attr"`
|
||||
Priority *int `xml:"priority,attr,omitempty"`
|
||||
Channel *string `xml:"channel,attr,omitempty"`
|
||||
Protocol *string `xml:"protocol,attr,omitempty"`
|
||||
Value *int `xml:"value,attr,omitempty"`
|
||||
Min *int `xml:"min,attr,omitempty"`
|
||||
Max *int `xml:"max,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
XMLName xml.Name `xml:"channel"`
|
||||
Name string `xml:"name,attr"`
|
||||
System string `xml:"system,attr"`
|
||||
Enabled bool `xml:"enabled,attr"`
|
||||
Site string `xml:"site,attr"`
|
||||
Order int `xml:"order,attr"`
|
||||
|
||||
AliasListName string `xml:"alias_list_name"`
|
||||
EventLogConfig EventLogConfig `xml:"event_log_configuration"`
|
||||
SourceConfig SourceConfig `xml:"source_configuration"`
|
||||
AuxDecodeConfig AuxDecodeConfig `xml:"aux_decode_configuration"`
|
||||
DecodeConfig DecodeConfig `xml:"decode_configuration"`
|
||||
RecordConfig RecordConfig `xml:"record_configuration"`
|
||||
}
|
||||
|
||||
type EventLogConfig struct {
|
||||
EventLogConfig []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
SourceConfig []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type AuxDecodeConfig struct {
|
||||
AuxDecodeConfig []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type DecodeConfig struct {
|
||||
DecodeConfig []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type RecordConfig struct {
|
||||
RecordConfig []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Stream struct {
|
||||
Attributes []xml.Attr `xml:",any,attr"`
|
||||
Stream []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func New() *Driver {
|
||||
return new(Driver)
|
||||
}
|
||||
|
||||
type Driver struct{}
|
||||
|
||||
func (st *Driver) ExportTalkgroups(ctx context.Context, w io.Writer, tgs []*talkgroups.Talkgroup, tmpl []byte) error {
|
||||
var pl Playlist
|
||||
|
||||
if tmpl != nil {
|
||||
err := xml.Unmarshal(tmpl, &pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pl.Aliases = nil
|
||||
}
|
||||
|
||||
for _, tg := range tgs {
|
||||
pl.Aliases = append(pl.Aliases, tgToAlias(tg))
|
||||
}
|
||||
|
||||
enc := xml.NewEncoder(w)
|
||||
enc.Indent("", " ")
|
||||
err := enc.Encode(&pl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return enc.Close()
|
||||
}
|
Loading…
Reference in a new issue