first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
.vscode
|
||||
@@ -0,0 +1,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
ListUsers(ctx context.Context) ([]UserInfo, error)
|
||||
LoginUser(ctx context.Context, username, password string) (*Result, error)
|
||||
SetupUser(ctx context.Context, session, username, preferredUsername, newPassword string) (*UserInfo, error)
|
||||
ValidateToken(ctx context.Context, token string) *UserInfo
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Provider Provider
|
||||
|
||||
userListMutex sync.Mutex
|
||||
userList []UserInfo
|
||||
userMap map[string]string
|
||||
userListTime time.Time
|
||||
|
||||
key struct{ Stuff uint64 }
|
||||
key2 struct{ Stuff2 string }
|
||||
}
|
||||
|
||||
func (s *Service) ValidateUser(ctx context.Context, token string) *UserInfo {
|
||||
return s.Provider.ValidateToken(ctx, token)
|
||||
}
|
||||
|
||||
func (s *Service) AddContextUser(ctx context.Context, user UserInfo) context.Context {
|
||||
return context.WithValue(ctx, &s.key, &user)
|
||||
}
|
||||
|
||||
func (s *Service) Users(ctx context.Context) ([]UserInfo, map[string]string, error) {
|
||||
s.userListMutex.Lock()
|
||||
if time.Since(s.userListTime) < time.Minute {
|
||||
m := s.userMap
|
||||
l := s.userList
|
||||
s.userListMutex.Unlock()
|
||||
return l, m, nil
|
||||
}
|
||||
s.userListMutex.Unlock()
|
||||
|
||||
users, err := s.Provider.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
s.userListMutex.Lock()
|
||||
s.userList = users
|
||||
s.userMap = make(map[string]string, len(users))
|
||||
for _, user := range users {
|
||||
s.userMap[user.ID] = user.Name
|
||||
}
|
||||
m := s.userMap
|
||||
s.userListTime = time.Now()
|
||||
s.userListMutex.Unlock()
|
||||
|
||||
return users, m, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUser(ctx context.Context) *UserInfo {
|
||||
v := ctx.Value(&s.key)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.(*UserInfo)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
User
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
User *UserInfo `json:"user"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Session string `json:"session,omitempty"`
|
||||
PasswordChangeRequired bool `json:"passwordChangeRequired"`
|
||||
}
|
||||
|
||||
func (user *UserInfo) HasIDOrPermission(userID, subject, action string) bool {
|
||||
return user.HasID(userID) || user.HasPermission(subject, action)
|
||||
}
|
||||
|
||||
func (user *UserInfo) HasID(id string) bool {
|
||||
return user != nil && user.ID == id
|
||||
}
|
||||
|
||||
func (user *UserInfo) HasOpPermission(userID, subject, action string) bool {
|
||||
if !user.HasID(userID) && !user.HasPermission(subject, "admin") {
|
||||
return false
|
||||
}
|
||||
|
||||
return user.HasPermission(subject, action)
|
||||
}
|
||||
|
||||
func (user *UserInfo) HasPermission(subject, action string) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
anyAll := "*.*"
|
||||
anyAction := fmt.Sprintf("%s.*", subject)
|
||||
anySubject := fmt.Sprintf("*.%s", action)
|
||||
specific := fmt.Sprintf("%s.%s", subject, action)
|
||||
|
||||
if action == "admin" {
|
||||
anyAction = specific
|
||||
}
|
||||
|
||||
for _, perm := range user.Permissions {
|
||||
if perm == anyAll || perm == anyAction || perm == anySubject || perm == specific {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/auth"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/tag"
|
||||
)
|
||||
|
||||
type Character struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"shortName"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Description string `json:"description"`
|
||||
FavoriteColor string `json:"favoriteColor"`
|
||||
WikiPage string `json:"wikiPage"`
|
||||
|
||||
Author auth.User `json:"author"`
|
||||
Attributes []Attribute `json:"attributes"`
|
||||
Relations []Relation `json:"relations"`
|
||||
Tags []Tag `json:"tags"`
|
||||
}
|
||||
|
||||
type Attribute struct {
|
||||
Label string `json:"label,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Relation struct {
|
||||
Label string `json:"label,omitempty"`
|
||||
Character
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Label string `json:"label,omitempty"`
|
||||
Restricted bool `json:"restricted,omitempty"`
|
||||
tag.Tag
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package rpdata2
|
||||
|
||||
import "fmt"
|
||||
|
||||
type NotFound string
|
||||
|
||||
func (e NotFound) Error() string {
|
||||
return fmt.Sprintf("%s not found", string(e))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module git.aiterp.net/rpdata2-take2/rpdata2
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc=
|
||||
github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,86 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
mathRand "math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ID generates an ID using crypto-random, falling back to math random when that fails
|
||||
// to avoid disrupting operation because of a faulty RNG.
|
||||
func ID(prefix string, length int) string {
|
||||
var data [32]byte
|
||||
|
||||
result := strings.Builder{}
|
||||
result.Grow(length + 32)
|
||||
result.WriteString(prefix)
|
||||
|
||||
pos := 0
|
||||
for result.Len() < length {
|
||||
if pos == 0 {
|
||||
randRead(data[:])
|
||||
}
|
||||
|
||||
result.WriteString(strconv.FormatUint(binary.BigEndian.Uint64(data[pos:pos+8]), 36))
|
||||
|
||||
pos = (pos + 8) % 32
|
||||
}
|
||||
|
||||
return result.String()[:length]
|
||||
}
|
||||
|
||||
func randRead(data []byte) {
|
||||
n, err := rand.Read(data)
|
||||
if err != nil {
|
||||
mathRand.Read(data[n:])
|
||||
}
|
||||
}
|
||||
|
||||
// InternalErrorID generates a long string
|
||||
func InternalErrorID() string {
|
||||
return ID("ISE", 32)
|
||||
}
|
||||
|
||||
// TagID generates a location ID. len=8
|
||||
func TagID(longName string) string {
|
||||
return friendlyID('T', longName, 8)
|
||||
}
|
||||
|
||||
// CharacterID generates a character ID. len=8
|
||||
func CharacterID(longName string) string {
|
||||
return friendlyID('C', longName, 8)
|
||||
}
|
||||
|
||||
func friendlyID(prefix byte, name string, length int) string {
|
||||
b := strings.Builder{}
|
||||
b.Grow(4)
|
||||
b.WriteByte(prefix)
|
||||
for _, ch := range strings.ToLower(name) {
|
||||
if ch >= 'a' && ch <= 'z' {
|
||||
b.WriteRune(ch)
|
||||
|
||||
if b.Len() > 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ID(b.String(), length)
|
||||
}
|
||||
|
||||
// StoryID generates a story ID: len=8
|
||||
func StoryID() string {
|
||||
return ID("S", 12)
|
||||
}
|
||||
|
||||
// PostID generates a post ID. len=12
|
||||
func PostID() string {
|
||||
return ID("P", 16)
|
||||
}
|
||||
|
||||
// AnnotationID generates an annotation.sql ID. len=12
|
||||
func AnnotationID() string {
|
||||
return ID("A", 16)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package genutils
|
||||
|
||||
func UpsertIntoArray[T comparable](arr []T, values ...T) []T {
|
||||
outer:
|
||||
for _, value := range values {
|
||||
for _, value2 := range arr {
|
||||
if value2 == value {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
arr = append(arr, value)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
func RemoveFromArray[T comparable](arr []T, values ...T) []T {
|
||||
for _, value := range values {
|
||||
for i, value2 := range arr {
|
||||
if value2 == value {
|
||||
arr = append(arr[:i], arr[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package genutils
|
||||
|
||||
func Ptr[T any](t T) *T {
|
||||
return &t
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package genutils
|
||||
|
||||
type Set[T comparable] struct {
|
||||
m map[T]bool
|
||||
a []T
|
||||
}
|
||||
|
||||
func (set *Set[T]) Add(values ...T) {
|
||||
if set.m == nil {
|
||||
set.m = make(map[T]bool, len(values)*4)
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if set.m[value] {
|
||||
continue
|
||||
}
|
||||
|
||||
set.m[value] = true
|
||||
set.a = append(set.a, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (set *Set[T]) Len() int {
|
||||
return len(set.a)
|
||||
}
|
||||
|
||||
func (set *Set[T]) Values() []T {
|
||||
return set.a[:len(set.a):len(set.a)]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package genutils
|
||||
|
||||
import "sort"
|
||||
|
||||
type Lesser[T any] interface {
|
||||
Less(T) bool
|
||||
}
|
||||
|
||||
func SortSlice[T Lesser[T]](slice []T) {
|
||||
sort.Slice(slice, func(i, j int) bool {
|
||||
return slice[i].Less(slice[j])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package genutils
|
||||
|
||||
type hasValid interface {
|
||||
Valid() bool
|
||||
}
|
||||
|
||||
func ApplyUpdate[T any](dst *T, value *T) {
|
||||
if value != nil {
|
||||
*dst = *value
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyUpdateValid[T hasValid](dst *T, value *T) {
|
||||
if value != nil && (*value).Valid() {
|
||||
*dst = *value
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyUpdateNonZero[T comparable](dst *T, value *T) {
|
||||
var zero T
|
||||
if value != nil && *value != zero {
|
||||
*dst = *value
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyUpdateNilZero[T comparable](dst **T, value *T) {
|
||||
if value != nil {
|
||||
var zero T
|
||||
|
||||
if *value == zero {
|
||||
*dst = nil
|
||||
} else {
|
||||
valueCopy := *value
|
||||
*dst = &valueCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyMapUpdate[K comparable, V any](dst *map[K]V, src map[K]*V) {
|
||||
if *dst == nil {
|
||||
dst = &map[K]V{}
|
||||
}
|
||||
for key, value := range src {
|
||||
if value != nil {
|
||||
(*dst)[key] = *value
|
||||
} else {
|
||||
delete(*dst, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyUpdateMapNilZero[K comparable, V comparable](dst *map[K]V, src map[K]V) {
|
||||
var zero V
|
||||
if *dst == nil {
|
||||
dst = &map[K]V{}
|
||||
}
|
||||
for key, value := range src {
|
||||
if value != zero {
|
||||
(*dst)[key] = value
|
||||
} else {
|
||||
delete(*dst, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApplyArrayUpdate[T comparable](arr *[]T, upsert []T, remove []T) {
|
||||
*arr = make([]T, 0, len(*arr)+len(upsert))
|
||||
*arr = UpsertIntoArray(*arr, upsert...)
|
||||
*arr = RemoveFromArray(*arr, remove...)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"git.aiterp.net/rpdata2/rpdata2-take2/ports/mysql/mysqlgen"
|
||||
"git.aiterp.net/rpdata2/rpdata2-take2/tag"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
q *mysqlgen.Queries
|
||||
}
|
||||
|
||||
func (db *Database) Tags() tag.Repository {
|
||||
return &tagRepository{
|
||||
db: db.db,
|
||||
q: db.q,
|
||||
}
|
||||
}
|
||||
|
||||
func Connect(host string, port int, username, password, database string) (*Database, error) {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf(
|
||||
"%s:%s@(%s:%d)/%s?parseTime=true", username, password, host, port, database,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(10)
|
||||
db.SetConnMaxIdleTime(time.Minute)
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q, err := mysqlgen.Prepare(context.Background(), db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Database{db: db, q: q}, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE tag
|
||||
(
|
||||
`id` CHAR(8) NOT NULL PRIMARY KEY,
|
||||
`parent_id` CHAR(8) NULL,
|
||||
`owner_id` VARCHAR(255) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`kind` INT NOT NULL,
|
||||
`description` TEXT NOT NULL,
|
||||
`listed` BOOL NOT NULL,
|
||||
`secret` BOOL NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE tag;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,128 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.13.0
|
||||
|
||||
package mysqlgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
q := Queries{db: db}
|
||||
var err error
|
||||
if q.deleteTagStmt, err = db.PrepareContext(ctx, deleteTag); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteTag: %w", err)
|
||||
}
|
||||
if q.findTagStmt, err = db.PrepareContext(ctx, findTag); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query FindTag: %w", err)
|
||||
}
|
||||
if q.listListedTagsStmt, err = db.PrepareContext(ctx, listListedTags); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListListedTags: %w", err)
|
||||
}
|
||||
if q.listTagsStmt, err = db.PrepareContext(ctx, listTags); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListTags: %w", err)
|
||||
}
|
||||
if q.replaceTagStmt, err = db.PrepareContext(ctx, replaceTag); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ReplaceTag: %w", err)
|
||||
}
|
||||
return &q, nil
|
||||
}
|
||||
|
||||
func (q *Queries) Close() error {
|
||||
var err error
|
||||
if q.deleteTagStmt != nil {
|
||||
if cerr := q.deleteTagStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteTagStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.findTagStmt != nil {
|
||||
if cerr := q.findTagStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing findTagStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listListedTagsStmt != nil {
|
||||
if cerr := q.listListedTagsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listListedTagsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listTagsStmt != nil {
|
||||
if cerr := q.listTagsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listTagsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.replaceTagStmt != nil {
|
||||
if cerr := q.replaceTagStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing replaceTagStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.ExecContext(ctx, args...)
|
||||
default:
|
||||
return q.db.ExecContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryRowContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryRowContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
tx *sql.Tx
|
||||
deleteTagStmt *sql.Stmt
|
||||
findTagStmt *sql.Stmt
|
||||
listListedTagsStmt *sql.Stmt
|
||||
listTagsStmt *sql.Stmt
|
||||
replaceTagStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
tx: tx,
|
||||
deleteTagStmt: q.deleteTagStmt,
|
||||
findTagStmt: q.findTagStmt,
|
||||
listListedTagsStmt: q.listListedTagsStmt,
|
||||
listTagsStmt: q.listTagsStmt,
|
||||
replaceTagStmt: q.replaceTagStmt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.13.0
|
||||
|
||||
package mysqlgen
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
ID string
|
||||
ParentID sql.NullString
|
||||
OwnerID string
|
||||
Name string
|
||||
Kind int
|
||||
Description string
|
||||
Listed bool
|
||||
Secret bool
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.13.0
|
||||
// source: tag.sql
|
||||
|
||||
package mysqlgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const deleteTag = `-- name: DeleteTag :exec
|
||||
DELETE FROM tag WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteTag(ctx context.Context, id string) error {
|
||||
_, err := q.exec(ctx, q.deleteTagStmt, deleteTag, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const findTag = `-- name: FindTag :one
|
||||
SELECT id, parent_id, owner_id, name, kind, description, listed, secret
|
||||
FROM tag
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) FindTag(ctx context.Context, id string) (Tag, error) {
|
||||
row := q.queryRow(ctx, q.findTagStmt, findTag, id)
|
||||
var i Tag
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ParentID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.Kind,
|
||||
&i.Description,
|
||||
&i.Listed,
|
||||
&i.Secret,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listListedTags = `-- name: ListListedTags :many
|
||||
SELECT id, parent_id, owner_id, name, kind, description, listed, secret
|
||||
FROM tag
|
||||
WHERE listed = 1
|
||||
`
|
||||
|
||||
func (q *Queries) ListListedTags(ctx context.Context) ([]Tag, error) {
|
||||
rows, err := q.query(ctx, q.listListedTagsStmt, listListedTags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Tag{}
|
||||
for rows.Next() {
|
||||
var i Tag
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ParentID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.Kind,
|
||||
&i.Description,
|
||||
&i.Listed,
|
||||
&i.Secret,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listTags = `-- name: ListTags :many
|
||||
SELECT id, parent_id, owner_id, name, kind, description, listed, secret
|
||||
FROM tag
|
||||
`
|
||||
|
||||
func (q *Queries) ListTags(ctx context.Context) ([]Tag, error) {
|
||||
rows, err := q.query(ctx, q.listTagsStmt, listTags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Tag{}
|
||||
for rows.Next() {
|
||||
var i Tag
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ParentID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.Kind,
|
||||
&i.Description,
|
||||
&i.Listed,
|
||||
&i.Secret,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const replaceTag = `-- name: ReplaceTag :exec
|
||||
REPLACE INTO tag (id, parent_id, owner_id, name, kind, description, listed, secret)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type ReplaceTagParams struct {
|
||||
ID string
|
||||
ParentID sql.NullString
|
||||
OwnerID string
|
||||
Name string
|
||||
Kind int
|
||||
Description string
|
||||
Listed bool
|
||||
Secret bool
|
||||
}
|
||||
|
||||
func (q *Queries) ReplaceTag(ctx context.Context, arg ReplaceTagParams) error {
|
||||
_, err := q.exec(ctx, q.replaceTagStmt, replaceTag,
|
||||
arg.ID,
|
||||
arg.ParentID,
|
||||
arg.OwnerID,
|
||||
arg.Name,
|
||||
arg.Kind,
|
||||
arg.Description,
|
||||
arg.Listed,
|
||||
arg.Secret,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/internal/genutils"
|
||||
)
|
||||
|
||||
func toNullString(s *string) sql.NullString {
|
||||
if s != nil {
|
||||
return sql.NullString{Valid: true, String: *s}
|
||||
} else {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
}
|
||||
|
||||
func fromNullString(ns sql.NullString) *string {
|
||||
if ns.Valid {
|
||||
return genutils.Ptr(ns.String)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
-- name: FindTag :one
|
||||
SELECT *
|
||||
FROM tag
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ListTags :many
|
||||
SELECT *
|
||||
FROM tag;
|
||||
|
||||
-- name: ListListedTags :many
|
||||
SELECT *
|
||||
FROM tag
|
||||
WHERE listed = 1;
|
||||
|
||||
-- name: ReplaceTag :exec
|
||||
REPLACE INTO tag (id, parent_id, owner_id, name, kind, description, listed, secret)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: DeleteTag :exec
|
||||
DELETE FROM tag WHERE id = ?;
|
||||
@@ -0,0 +1,20 @@
|
||||
version: "1"
|
||||
packages:
|
||||
- name: "mysqlgen"
|
||||
path: "./mysqlgen"
|
||||
queries: "./queries"
|
||||
schema: "./migrations"
|
||||
engine: "mysql"
|
||||
emit_prepared_queries: true
|
||||
emit_interface: false
|
||||
emit_exact_table_names: false
|
||||
emit_empty_slices: true
|
||||
emit_json_tags: false
|
||||
overrides:
|
||||
- go_type: "float64"
|
||||
db_type: "float"
|
||||
- go_type: "float64"
|
||||
db_type: "float"
|
||||
nullable: true
|
||||
- go_type: "int"
|
||||
db_type: "int"
|
||||
@@ -0,0 +1,125 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/ports/mysql/mysqlgen"
|
||||
"git.aiterp.net/rpdata2-take2/rpdata2/tag"
|
||||
"github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
type tagRepository struct {
|
||||
db *sql.DB
|
||||
q *mysqlgen.Queries
|
||||
}
|
||||
|
||||
func (r *tagRepository) FindOne(ctx context.Context, id string) (*tag.Tag, error) {
|
||||
row, err := r.q.FindTag(ctx, id)
|
||||
if err != sql.ErrNoRows {
|
||||
return nil, rpdata2.NotFound("Tag")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tag.Tag{
|
||||
ID: row.ID,
|
||||
ParentID: fromNullString(row.ParentID),
|
||||
OwnerID: row.OwnerID,
|
||||
Name: row.Name,
|
||||
Kind: tag.Kind(row.Kind),
|
||||
Description: row.Description,
|
||||
Listed: row.Listed,
|
||||
Secret: row.Secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *tagRepository) FindRecursive(ctx context.Context, id string) ([]tag.Tag, error) {
|
||||
return r.findRecursive(ctx, make([]tag.Tag, 0, 32), []string{id})
|
||||
}
|
||||
|
||||
func (r *tagRepository) findRecursive(ctx context.Context, tags []tag.Tag, parentIDs []string) ([]tag.Tag, error) {
|
||||
query, args, err := squirrel.Select("id,parent_id,owner_id,name,kind,description,listed,secret").
|
||||
From("tag").
|
||||
Where(squirrel.Eq{"parent_id": parentIDs}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
nextParentIDs := make([]string, 0, 8)
|
||||
for rows.Next() {
|
||||
var t tag.Tag
|
||||
var ns sql.NullString
|
||||
|
||||
err = rows.Scan(&t.ID, &ns, &t.OwnerID, &t.Name, &t.Kind, &t.Description, &t.Listed, &t.Secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.ParentID = fromNullString(ns)
|
||||
nextParentIDs = append(nextParentIDs, t.ID)
|
||||
|
||||
tags = append(tags, t)
|
||||
}
|
||||
|
||||
if len(nextParentIDs) > 0 {
|
||||
return r.findRecursive(ctx, tags, nextParentIDs)
|
||||
} else {
|
||||
return tags, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tagRepository) List(ctx context.Context, full bool) ([]tag.Tag, error) {
|
||||
var rows []mysqlgen.Tag
|
||||
var err error
|
||||
if full {
|
||||
rows, err = r.q.ListTags(ctx)
|
||||
} else {
|
||||
rows, err = r.q.ListListedTags(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []tag.Tag{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]tag.Tag, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
res = append(res, tag.Tag{
|
||||
ID: row.ID,
|
||||
ParentID: fromNullString(row.ParentID),
|
||||
OwnerID: row.OwnerID,
|
||||
Name: row.Name,
|
||||
Kind: tag.Kind(row.Kind),
|
||||
Description: row.Description,
|
||||
Listed: row.Listed,
|
||||
Secret: row.Secret,
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *tagRepository) Save(ctx context.Context, tag tag.Tag) error {
|
||||
return r.q.ReplaceTag(ctx, mysqlgen.ReplaceTagParams{
|
||||
ID: tag.ID,
|
||||
ParentID: toNullString(tag.ParentID),
|
||||
OwnerID: tag.OwnerID,
|
||||
Name: tag.Name,
|
||||
Kind: int(tag.Kind),
|
||||
Description: tag.Description,
|
||||
Listed: tag.Listed,
|
||||
Secret: tag.Secret,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *tagRepository) Delete(ctx context.Context, tag tag.Tag) error {
|
||||
return r.q.DeleteTag(ctx, tag.ID)
|
||||
}
|
||||
@@ -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