first commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package tag
|
||||
|
||||
import "context"
|
||||
|
||||
type Repository interface {
|
||||
FindOne(ctx context.Context, id string) (*Tag, error)
|
||||
FindRecursive(ctx context.Context, id string) ([]Tag, error)
|
||||
List(ctx context.Context, full bool) ([]Tag, error)
|
||||
Save(ctx context.Context, tag Tag) error
|
||||
Delete(ctx context.Context, tag Tag) error
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/auth"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Repository Repository
|
||||
Auth auth.Service
|
||||
}
|
||||
|
||||
func (s *Service) Find(ctx context.Context, id string) (*Node, error) {
|
||||
tags, err := s.Repository.FindRecursive(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root := BuildForest(tags)[0].WithoutSecret(s.Auth.GetUser(ctx))
|
||||
if root == nil {
|
||||
return nil, rpdata2.NotFound("Tag")
|
||||
}
|
||||
|
||||
return root, nil
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package tag
|
||||
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
ParentID *string `json:"parentId"`
|
||||
OwnerID string `json:"ownerId"` // The user who can change the tag
|
||||
Name string `json:"name"`
|
||||
Kind Kind `json:"kind"`
|
||||
Description string `json:"description"`
|
||||
Listed bool `json:"listed"` // Whether to list it for search
|
||||
Secret bool `json:"secret"` // Only owners can see it
|
||||
}
|
||||
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
// TKOrganization is an organization, faction or group
|
||||
TKOrganization = 1 << iota
|
||||
// TKLocation is a settlement, building, planet, etc.... Icon: Pin
|
||||
TKLocation
|
||||
// TKEvent is a specific event
|
||||
TKEvent
|
||||
// TKPlot is a larger plot
|
||||
TKPlot
|
||||
// TKSeries is a thread of content that's not exactly a plot/event
|
||||
TKSeries
|
||||
)
|
||||
|
||||
/*
|
||||
L Aite
|
||||
L Freedom Falls
|
||||
LO Miner's Respite
|
||||
L Derrai Union
|
||||
L Derrai
|
||||
L Aroste
|
||||
L Aroste
|
||||
P "The Collectors"
|
||||
|
||||
P "Rellis Stuff"
|
||||
E Rellis' Contract
|
||||
E Familial Ties
|
||||
E Skipping Work
|
||||
E Sala's Backlog
|
||||
S Anywhere but Here
|
||||
|
||||
*/
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/auth"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/internal/genutils"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Tag
|
||||
|
||||
Children []Node `json:"children"`
|
||||
}
|
||||
|
||||
func (n Node) WithoutSecret(reqUser *auth.UserInfo) *Node {
|
||||
return n.filter(func(n Node) bool {
|
||||
if !n.Secret {
|
||||
return false
|
||||
}
|
||||
|
||||
return reqUser != nil && reqUser.HasIDOrPermission(n.OwnerID, "tag", "view_secret")
|
||||
})
|
||||
}
|
||||
|
||||
func (n Node) WithoutUnlisted() *Node {
|
||||
return n.filter(func(n Node) bool { return n.Listed })
|
||||
}
|
||||
|
||||
func (n Node) filter(cb func(n Node) bool) *Node {
|
||||
if !cb(n) {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := make([]Node, 0, len(n.Children))
|
||||
for _, child := range n.Children {
|
||||
pruned := child.filter(cb)
|
||||
if pruned == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered = append(filtered, *pruned)
|
||||
}
|
||||
|
||||
n.Children = filtered
|
||||
return &n
|
||||
}
|
||||
|
||||
func (n Node) Less(n2 Node) bool {
|
||||
if n.Kind < n2.Kind {
|
||||
return true
|
||||
}
|
||||
|
||||
return n.Name < n2.Name
|
||||
}
|
||||
|
||||
func BuildForest(tags []Tag) []Node {
|
||||
nodes := make(map[string]*Node)
|
||||
for _, tag := range tags {
|
||||
nodes[tag.ID] = &Node{Tag: tag, Children: []Node{}}
|
||||
}
|
||||
|
||||
children := make(map[string][]*Node, len(tags))
|
||||
for _, node := range nodes {
|
||||
if node.ParentID != nil {
|
||||
children[*node.ParentID] = append(children[*node.ParentID], node)
|
||||
}
|
||||
}
|
||||
|
||||
cleared := make(map[string]bool)
|
||||
for len(cleared) < len(nodes) {
|
||||
anyCleared := false
|
||||
for _, node := range nodes {
|
||||
if cleared[node.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
good := true
|
||||
for _, child := range children[node.ID] {
|
||||
if !cleared[child.ID] {
|
||||
good = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if good {
|
||||
genutils.SortSlice(node.Children)
|
||||
|
||||
cleared[node.ID] = true
|
||||
anyCleared = true
|
||||
if node.ParentID != nil {
|
||||
nodes[*node.ParentID].Children = append(nodes[*node.ParentID].Children, *node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !anyCleared {
|
||||
panic("deadlock in tag.BuildForest")
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]Node, 0, 8)
|
||||
for _, node := range nodes {
|
||||
if node.ParentID == nil {
|
||||
res = append(res, *node)
|
||||
}
|
||||
}
|
||||
|
||||
genutils.SortSlice(res)
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/internal/genutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func printNode(t *testing.T, prefix string, node Node) {
|
||||
t.Log(prefix + "[" + node.ID + "] " + node.Name)
|
||||
for _, child := range node.Children {
|
||||
printNode(t, prefix+" ", child)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_Less(t *testing.T) {
|
||||
n1 := Node{Tag: Tag{Kind: TKLocation | TKOrganization, Name: "Miner's Respite"}}
|
||||
n2 := Node{Tag: Tag{Kind: TKLocation | TKOrganization, Name: "Litae's Grace"}}
|
||||
n3 := Node{Tag: Tag{Kind: TKOrganization, Name: "Redrock Agency"}}
|
||||
n4 := Node{Tag: Tag{Kind: TKLocation, Name: "Redrock HQ"}}
|
||||
|
||||
assert.True(t, n2.Less(n1))
|
||||
assert.True(t, !n1.Less(n2))
|
||||
assert.True(t, n3.Less(n4))
|
||||
}
|
||||
|
||||
func TestNode_WithoutUnlisted(t *testing.T) {
|
||||
before := Node{Tag{Name: "A", Listed: true}, []Node{
|
||||
{Tag{Name: "B", Listed: true}, []Node{
|
||||
{Tag{Name: "D", Listed: false}, []Node{
|
||||
{Tag{Name: "F", Listed: true}, []Node{}},
|
||||
}},
|
||||
{Tag{Name: "E", Secret: true, Listed: true}, []Node{
|
||||
{Tag{Name: "G", Listed: true}, []Node{}},
|
||||
}},
|
||||
}},
|
||||
{Tag{Name: "C", Listed: true}, []Node{}},
|
||||
}}
|
||||
|
||||
after := Node{Tag{Name: "A", Listed: true}, []Node{
|
||||
{Tag{Name: "B", Listed: true}, []Node{
|
||||
{Tag{Name: "E", Secret: true, Listed: true}, []Node{
|
||||
{Tag{Name: "G", Listed: true}, []Node{}},
|
||||
}},
|
||||
}},
|
||||
{Tag{Name: "C", Listed: true}, []Node{}},
|
||||
}}
|
||||
|
||||
assert.Equal(t, &after, before.WithoutUnlisted())
|
||||
}
|
||||
|
||||
func TestBuildForest(t *testing.T) {
|
||||
tags := []Tag{
|
||||
{ID: "T0", ParentID: genutils.Ptr("T7"), Name: "Root"},
|
||||
{ID: "T1", ParentID: genutils.Ptr("T0"), Name: "Stuff"},
|
||||
{ID: "T2", ParentID: genutils.Ptr("T0"), Name: "Things"},
|
||||
{ID: "T3", Name: "Second Root"},
|
||||
{ID: "T4", ParentID: genutils.Ptr("T2"), Name: "Items"},
|
||||
{ID: "T5", ParentID: genutils.Ptr("T1"), Name: "Objects"},
|
||||
{ID: "T6", ParentID: genutils.Ptr("T4"), Name: "Thingies"},
|
||||
{ID: "T7", Name: "Real Root"},
|
||||
{ID: "T8", ParentID: genutils.Ptr("T3"), Name: "Another Leaf"},
|
||||
{ID: "T9", ParentID: genutils.Ptr("T5"), Name: "Branch"},
|
||||
{ID: "TA", ParentID: genutils.Ptr("T9"), Name: "Leafy Leaf"},
|
||||
}
|
||||
|
||||
nodes := BuildForest(tags)
|
||||
for _, node := range nodes {
|
||||
printNode(t, "", node)
|
||||
}
|
||||
|
||||
expected := []Node{
|
||||
{tags[7], []Node{
|
||||
{tags[0], []Node{
|
||||
{tags[1], []Node{
|
||||
{tags[5], []Node{
|
||||
{tags[9], []Node{
|
||||
{tags[10], []Node{}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
{tags[2], []Node{
|
||||
{tags[4], []Node{
|
||||
{tags[6], []Node{}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
{tags[3], []Node{
|
||||
{tags[8], []Node{}},
|
||||
}},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, nodes)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tag
|
||||
|
||||
import "git.aiterp.net/rpdata2-take2/rpdata2/internal/genutils"
|
||||
|
||||
type Update struct {
|
||||
ParentID *string `json:"parentId"`
|
||||
Name *string `json:"name"`
|
||||
Kind *Kind `json:"kind"`
|
||||
Listed *bool `json:"listed"`
|
||||
Secret *bool `json:"secret"` // Only owners can see it
|
||||
}
|
||||
|
||||
func (t *Tag) ApplyUpdate(update *Update) {
|
||||
genutils.ApplyUpdateNilZero(&t.ParentID, update.ParentID)
|
||||
genutils.ApplyUpdate(&t.Name, update.Name)
|
||||
genutils.ApplyUpdate(&t.Kind, update.Kind)
|
||||
genutils.ApplyUpdate(&t.Listed, update.Listed)
|
||||
genutils.ApplyUpdate(&t.Secret, update.Secret)
|
||||
}
|
||||
Reference in New Issue
Block a user