first commit

This commit is contained in:
2023-06-27 09:48:33 +02:00
commit 8fc0220e51
29 changed files with 1315 additions and 0 deletions
+11
View File
@@ -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
}
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+95
View File
@@ -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)
}
+19
View File
@@ -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)
}