package mercury
import (
"fmt"
"path/filepath"
"strings"
"golang.org/x/exp/maps"
)
type Config []*Space
func NewConfig(spaces ...*Space) Config {
return spaces
}
func (c *Config) AddSpace(spaces ...*Space) *Config {
*c = append(*c, spaces...)
return c
}
// Len implements Len for sort.interface
func (lis Config) Len() int {
return len(lis)
}
// Less implements Less for sort.interface
func (lis Config) Less(i, j int) bool {
return lis[i].Space < lis[j].Space
}
// Swap implements Swap for sort.interface
func (lis Config) Swap(i, j int) { lis[i], lis[j] = lis[j], lis[i] }
// StringList returns the space names as a list
func (lis Config) StringList() string {
var buf strings.Builder
for _, o := range lis {
if len(o.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteRune('\n')
}
buf.WriteRune('@')
buf.WriteString(o.Space)
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString(strings.Join(o.Tags, " "))
}
buf.WriteRune('\n')
}
return buf.String()
}
// ToSpaceMap formats as SpaceMap
func (lis Config) ToSpaceMap() SpaceMap {
out := make(SpaceMap)
for _, c := range lis {
out[c.Space] = c
}
return out
}
// String format config as string
func (lis Config) String() string {
var buf strings.Builder
for i, o := range lis {
attLen := 0
tagLen := 0
if i > 0 {
buf.WriteRune('\n')
}
for _, v := range o.List {
l := len(v.Name)
if attLen <= l {
attLen = l
}
t := len(strings.Join(v.Tags, " "))
if tagLen <= t {
tagLen = t
}
}
if len(o.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteRune('\n')
}
buf.WriteRune('@')
buf.WriteString(o.Space)
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString(strings.Join(o.Tags, " "))
}
buf.WriteRune('\n')
for _, v := range o.List {
if len(v.Notes) > 0 {
buf.WriteString("# ")
buf.WriteString(strings.Join(v.Notes, "\n# "))
buf.WriteString("\n")
}
buf.WriteString(v.Name)
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
if len(v.Tags) > 0 {
t := strings.Join(v.Tags, " ")
buf.WriteString(t)
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
} else {
buf.WriteString(strings.Repeat(" ", tagLen+1))
}
switch len(v.Values) {
case 0:
buf.WriteString("\n")
case 1:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
default:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
for _, s := range v.Values[1:] {
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
buf.WriteString(":")
buf.WriteString(s)
buf.WriteString("\n")
}
}
}
for _, line := range o.Trailer {
buf.WriteString(line)
buf.WriteRune('\n')
}
}
return buf.String()
}
// EnvString format config as environ
func (lis Config) EnvString() string {
var buf strings.Builder
for _, o := range lis {
for _, v := range o.List {
buf.WriteString(o.Space)
for _, t := range o.Tags {
buf.WriteRune(' ')
buf.WriteString(t)
}
buf.WriteRune(':')
buf.WriteString(v.Name)
for _, t := range v.Tags {
buf.WriteRune(' ')
buf.WriteString(t)
}
switch len(v.Values) {
case 0:
buf.WriteRune('=')
buf.WriteRune('\n')
case 1:
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
default:
buf.WriteRune('+')
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
for _, s := range v.Values[1:] {
buf.WriteString(o.Space)
buf.WriteRune(':')
buf.WriteString(v.Name)
buf.WriteRune('+')
buf.WriteRune('=')
buf.WriteString(s)
buf.WriteRune('\n')
}
}
}
}
return buf.String()
}
// INIString format config as ini
func (lis Config) INIString() string {
var buf strings.Builder
for _, o := range lis {
for _, note := range o.Notes {
buf.WriteString("; ")
buf.WriteString(note)
buf.WriteRune('\n')
}
buf.WriteRune('[')
buf.WriteString(o.Space)
buf.WriteRune(']')
buf.WriteRune('\n')
for _, v := range o.List {
for _, note := range v.Notes {
buf.WriteString("; ")
buf.WriteString(note)
buf.WriteRune('\n')
}
buf.WriteString(v.Name)
switch len(v.Values) {
case 0:
buf.WriteRune('=')
buf.WriteRune('\n')
case 1:
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
default:
buf.WriteRune('[')
buf.WriteRune('0')
buf.WriteRune(']')
buf.WriteRune('=')
buf.WriteString(v.Values[0])
buf.WriteRune('\n')
for i, s := range v.Values[1:] {
buf.WriteString(v.Name)
buf.WriteRune('[')
buf.WriteString(fmt.Sprintf("%d", i))
buf.WriteRune(']')
buf.WriteRune('=')
buf.WriteString(s)
buf.WriteRune('\n')
}
}
}
for _, line := range o.Trailer {
buf.WriteString("; ")
buf.WriteString(line)
buf.WriteRune('\n')
}
buf.WriteRune('\n')
}
return buf.String()
}
// String format config as string
func (lis Config) HTMLString() string {
var buf strings.Builder
for i, o := range lis {
attLen := 0
tagLen := 0
if i > 0 {
buf.WriteRune('\n')
}
for _, v := range o.List {
l := len(v.Name)
if attLen <= l {
attLen = l
}
t := len(strings.Join(v.Tags, " "))
if tagLen <= t {
tagLen = t
}
}
if len(o.Notes) > 0 {
buf.WriteString("")
buf.WriteString("# ")
buf.WriteString(strings.Join(o.Notes, "\n# "))
buf.WriteString("")
buf.WriteRune('\n')
}
buf.WriteString("")
buf.WriteRune('@')
buf.WriteString(o.Space)
buf.WriteString("")
if len(o.Tags) > 0 {
buf.WriteRune(' ')
buf.WriteString("")
buf.WriteString(strings.Join(o.Tags, " "))
buf.WriteString("")
}
buf.WriteRune('\n')
for _, v := range o.List {
if len(v.Notes) > 0 {
buf.WriteString("")
buf.WriteString("# ")
buf.WriteString(strings.Join(v.Notes, "\n# "))
buf.WriteString("")
buf.WriteString("\n")
}
buf.WriteString("")
buf.WriteString(v.Name)
buf.WriteString("")
buf.WriteString(strings.Repeat(" ", attLen-len(v.Name)+1))
if len(v.Tags) > 0 {
t := strings.Join(v.Tags, " ")
buf.WriteString("")
buf.WriteString(t)
buf.WriteString(strings.Repeat(" ", tagLen-len(t)+1))
buf.WriteString("")
} else {
buf.WriteString(strings.Repeat(" ", tagLen+1))
}
switch len(v.Values) {
case 0:
buf.WriteString("\n")
case 1:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
default:
buf.WriteString(" :")
buf.WriteString(v.Values[0])
buf.WriteString("\n")
for _, s := range v.Values[1:] {
buf.WriteString(strings.Repeat(" ", attLen+tagLen+3))
buf.WriteString(":")
buf.WriteString(s)
buf.WriteString("\n")
}
}
}
for _, line := range o.Trailer {
buf.WriteString("")
buf.WriteString(line)
buf.WriteString("")
buf.WriteRune('\n')
}
}
return buf.String()
}
// Space stores a registry of spaces
type Space struct {
Space string `json:"space"`
Tags []string `json:"tags,omitempty"`
Notes []string `json:"notes,omitempty"`
List []Value `json:"list,omitempty"`
Trailer []string `json:"trailer,omitempty"`
}
func NewSpace(space string) *Space {
return &Space{Space: space}
}
// HasTag returns true if needle is found
// If the needle ends with a / it will be treated
// as a prefix for tag meta data.
func (s *Space) HasTag(needle string) bool {
isPrefix := strings.HasSuffix(needle, "/")
for i := range s.Tags {
switch isPrefix {
case true:
if strings.HasPrefix(s.Tags[i], needle) {
return true
}
case false:
if s.Tags[i] == needle {
return true
}
}
}
return false
}
// GetTagMeta retuns the value after a '/' in a tag.
// Tags are in the format 'name' or 'name/value'
// This function returns the value.
func (s *Space) GetTagMeta(needle string, offset int) string {
if !strings.HasSuffix(needle, "/") {
needle += "/"
}
for i := range s.Tags {
if strings.HasPrefix(s.Tags[i], needle) {
if offset > 0 {
offset--
continue
}
return strings.TrimPrefix(s.Tags[i], needle)
}
}
return ""
}
// FirstTagMeta returns the first meta tag value.
func (s *Space) FirstTagMeta(needle string) string {
return s.GetTagMeta(needle, 0)
}
// GetValues that match name
func (s *Space) GetValues(name string) (lis []Value) {
for i := range s.List {
if s.List[i].Name == name {
lis = append(lis, s.List[i])
}
}
return
}
// FirstValue that matches name
func (s *Space) FirstValue(name string) Value {
for i := range s.List {
if s.List[i].Name == name {
return s.List[i]
}
}
return Value{}
}
func (s *Space) SetTags(tags ...string) *Space {
s.Tags = tags
return s
}
func (s *Space) AddTags(tags ...string) *Space {
s.Tags = append(s.Tags, tags...)
return s
}
func (s *Space) SetNotes(notes ...string) *Space {
s.Notes = notes
return s
}
func (s *Space) AddNotes(notes ...string) *Space {
s.Notes = append(s.Notes, notes...)
return s
}
func (s *Space) SetKeys(keys ...*Value) *Space {
for i := range keys {
k := *keys[i]
k.Seq = uint64(i)
s.List = append(s.List, k)
}
return s
}
func (s *Space) AddKeys(keys ...*Value) *Space {
l := uint64(len(s.List))
for i := range keys {
k := *keys[i]
k.Seq = uint64(i) + l
s.List = append(s.List, k)
}
return s
}
// SpaceMap generic map of space values
type SpaceMap map[string]*Space
func (m SpaceMap) Space(name string) (*Space, bool) {
s, ok := m[name]
return s, ok
}
// Rule is a type of rule
type Rule struct {
Role string
Type string
Match string
}
// Rules is a list of rules
type Rules []Rule
// GetNamespaceSearch returns a default search for users rules.
func (r Rules) GetNamespaceSearch() (lis NamespaceSearch) {
for _, o := range r {
if o.Type == "NS" && (o.Role == "read" || o.Role == "write") {
lis = append(lis, NamespaceStar(o.Match))
}
}
return
}
// Check if name matches rule
func (r Rule) Check(name string) bool {
ok, err := filepath.Match(r.Match, name)
if err != nil {
return false
}
return ok
}
// CheckNamespace verifies user has access
func (r Rules) CheckNamespace(search NamespaceSearch) bool {
for _, ns := range search {
if !r.GetRoles("NS", ns.Value()).HasRole("read", "write") {
return false
}
}
return true
}
func (r Rules) Less(i, j int) bool {
si, sj := scoreRule(r[i]), scoreRule(r[j])
if si != sj {
return si < sj
}
return len(r[i].Match) < len(r[j].Match)
}
func (r Rules) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r Rules) Len() int { return len(r) }
func scoreRule(r Rule) int {
score := 0
if r.Type == "GR" {
score += 1000
}
switch r.Role {
case "admin":
score += 100
case "write":
score += 50
case "read":
score += 10
}
return score
}
// ReduceSearch verifies user has access
func (r Rules) ReduceSearch(search NamespaceSearch) (out NamespaceSearch) {
rules := r.GetNamespaceSearch()
skip := make(map[string]struct{})
out = make(NamespaceSearch, 0, len(rules))
for _, rule := range rules {
if _, ok := skip[rule.Raw()]; ok {
continue
}
for _, ck := range search {
if _, ok := skip[ck.Raw()]; ok {
continue
} else if rule.Match(ck.Raw()) {
skip[ck.Raw()] = struct{}{}
out = append(out, ck)
} else if ck.Match(rule.Raw()) {
out = append(out, rule)
}
}
}
return
}
// Roles is a list of roles for a resource
type Roles map[string]struct{}
// GetRoles returns a list of Roles
func (r Rules) GetRoles(typ, name string) (lis Roles) {
lis = make(Roles)
for _, o := range r {
if typ == o.Type && o.Check(name) {
lis[o.Role] = struct{}{}
}
}
return
}
// HasRole is a valid role
func (r Roles) HasRole(roles ...string) bool {
for _, role := range roles {
if _, ok := r[role]; ok {
return true
}
}
return false
}
// ToArray converts SpaceMap to ArraySpace
func (m SpaceMap) ToArray() Config {
a := make(Config, 0, len(m))
for _, s := range m {
a = append(a, s)
}
return a
}
func (m *SpaceMap) MergeMap(s SpaceMap) {
m.Merge(maps.Values(s)...)
}
func (m *SpaceMap) Merge(lis ...*Space) {
for _, s := range lis {
// Only accept first version based on priority.
if _, ok := (*m)[s.Space]; ok {
continue
}
(*m)[s.Space] = s
// // Merge values together.
// c, ok := (*m)[s.Space]
// if ok {
// c = &Space{}
// }
// c.Notes = append(c.Notes, s.Notes...)
// c.Tags = append(c.Tags, s.Tags...)
// last := c.List[len(c.List)-1].Seq
// for i := range s.List {
// v := s.List[i]
// v.Seq += last
// c.List = append(c.List, v)
// }
// (*m)[s.Space] = c
}
}
// Value stores the attributes for space values
type Value struct {
Space string `json:"-" db:"space"`
Seq uint64 `json:"-" db:"seq"`
Name string `json:"name"`
Values []string `json:"values"`
Notes []string `json:"notes"`
Tags []string `json:"tags"`
}
// func (v *Value) ID() string {
// return gql.FmtID("MercurySpace:%v:%v", v.Space, v.Seq)
// }
// HasTag returns true if needle is found
// If the needle ends with a / it will be treated
// as a prefix for tag meta data.
func (v Value) HasTag(needle string) bool {
isPrefix := strings.HasSuffix(needle, "/")
for i := range v.Tags {
switch isPrefix {
case true:
if strings.HasPrefix(v.Tags[i], needle) {
return true
}
case false:
if v.Tags[i] == needle {
return true
}
}
}
return false
}
// GetTagMeta retuns the value after a '/' in a tag.
// Tags are in the format 'name' or 'name/value'
// This function returns the value.
func (v Value) GetTagMeta(needle string, offset int) string {
if !strings.HasSuffix(needle, "/") {
needle += "/"
}
for i := range v.Tags {
if strings.HasPrefix(v.Tags[i], needle) {
if offset > 0 {
offset--
continue
}
return strings.TrimPrefix(v.Tags[i], needle)
}
}
return ""
}
// FirstTagMeta returns the first meta tag value.
func (v Value) FirstTagMeta(needle string) string {
return v.GetTagMeta(needle, 0)
}
// First value in array.
func (v Value) First() string {
if len(v.Values) < 1 {
return ""
}
return v.Values[0]
}
// Join values with newlines.
func (v Value) Join() string {
return strings.Join(v.Values, "\n")
}
func NewValue(name string) *Value {
return &Value{Name: name}
}
func (v *Value) SetTags(tags ...string) *Value {
v.Tags = tags
return v
}
func (v *Value) AddTags(tags ...string) *Value {
v.Tags = append(v.Tags, tags...)
return v
}
func (v *Value) SetNotes(notes ...string) *Value {
v.Notes = notes
return v
}
func (v *Value) AddNotes(notes ...string) *Value {
v.Notes = append(v.Notes, notes...)
return v
}
func (v *Value) SetValues(values ...string) *Value {
v.Values = values
return v
}
func (v *Value) AddValues(values ...string) *Value {
v.Values = append(v.Values, values...)
return v
}