Skip to content

Commit

Permalink
clean-up some xref and relation/target stuff
Browse files Browse the repository at this point in the history
Signed-off-by: Doug Davis <[email protected]>
  • Loading branch information
duglin committed Jan 8, 2025
1 parent 0a76520 commit cc74adc
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 160 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,4 @@ TODOs:
- Split the model.verify stuff so it doesn't verify the data unless asked to
- make sure that setting capabilities.BOOL-attrs to 'true' fails if we don't
support it. Like pagination or enforcecompatibility
- support/test ?inline, ?filter and ?export on write operations
2 changes: 1 addition & 1 deletion cmds/server/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func LoadDirsSample(reg *registry.Registry) *registry.Registry {
_, err = rm.AddAttr("*", registry.STRING)
ErrFatalf(err)

_, err = reg.Model.AddAttrRelation("resptr", "/dirs/files/versions?")
_, err = reg.Model.AddAttrRelation("resptr", "/dirs/files[/versions]")
ErrFatalf(err)

ErrFatalf(reg.Model.Verify())
Expand Down
28 changes: 22 additions & 6 deletions registry/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -1085,13 +1085,26 @@ var OrderedSpecProps = []*Attribute{
return nil
},
updateFn: func(e *Entity) error {
// Very special, if we're in meta and xref set then
// erase 'epoch'. We can't do it earlier because we need
// the checkFn to be run to make sure any incoming value
// was valid
if e.Type == ENTITY_META && e.GetAsString("xref") != "" {
e.NewObject["epoch"] = nil
return nil
}

// If we already set Epoch in this Tx, just exit
if e.EpochSet {
// If we already set epoch this tx but there's no value
// then grab it from Object, otherwise we'll be missing a
// value during Save(). This can happen when we Save()
// more than once on this Entity during the same Tx and
// the 2nd Save() didn't have epoch as part of the incoming
// Object
if IsNil(e.NewObject["epoch"]) {
e.NewObject["epoch"] = e.Object["epoch"]
}
return nil
}

Expand Down Expand Up @@ -2453,7 +2466,9 @@ func PrepUpdateEntity(e *Entity) error {

// If no match then return an error saying why
func (e *Entity) MatchRelation(str string, relation string) error {
targetParts := strings.Split(relation, "/")
// 0=all 1=GROUPS 2=RESOURCES 3=versions|"" 4=[/versions]|""
targetParts := targetRE.FindStringSubmatch(relation)

if len(str) == 0 {
return fmt.Errorf("must be an xid, not empty")
}
Expand Down Expand Up @@ -2490,15 +2505,15 @@ func (e *Entity) MatchRelation(str string, relation string) error {
relation, strParts[2])
}

if len(targetParts) == 2 { // ""/GROUPS
if targetParts[2] == "" { // /GROUPS
if len(strParts) == 3 {
return nil
}
return fmt.Errorf("must match %q target, extra stuff after %q",
relation, strParts[2])
}

// len targetParts >= 3
// targetParts has RESOURCES
if len(strParts) < 4 { // /GROUPS/gID/RESOURCES
return fmt.Errorf("must match %q target, missing %q",
relation, targetParts[2])
Expand All @@ -2522,7 +2537,8 @@ func (e *Entity) MatchRelation(str string, relation string) error {
return fmt.Errorf("must match %q target, %q isn't a valid ID",
relation, strParts[4])
}
if len(targetParts) == 3 {

if targetParts[3] == "" && targetParts[4] == "" {
if len(strParts) == 5 {
return nil
}
Expand All @@ -2531,9 +2547,9 @@ func (e *Entity) MatchRelation(str string, relation string) error {

}

if targetParts[3] == "versions?" {
if targetParts[4] != "" { // has [/versions]
if len(strParts) == 5 {
// /GROUPS/RESOURCES/version? vs /GROUPS/gID/RESOURCES/rID
// /GROUPS/RESOURCES[/version] vs /GROUPS/gID/RESOURCES/rID
return nil
}
}
Expand Down
9 changes: 9 additions & 0 deletions registry/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package registry

import (
"fmt"
"strings"

log "github.com/duglin/dlog"
)
Expand Down Expand Up @@ -314,6 +315,14 @@ func (g *Group) UpsertResourceWithObject(rType string, id string, vID string, ob
PanicIf(err != nil, "No meta %q: %s", r.UID, err)

if !IsNil(meta.Get("xref")) {
delete(obj, "meta")
delete(obj, r.Singular+"id")
if len(obj) > 0 {
return nil, false,
fmt.Errorf("Extra attributes (%s) not allowed when "+
"\"xref\" is set", strings.Join(SortedKeys(obj), ","))
}

// All versions should have been deleted already so just return
return r, isNew, nil
}
Expand Down
53 changes: 24 additions & 29 deletions registry/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
log "github.com/duglin/dlog"
)

var RegexpModelName = regexp.MustCompile("^[a-z_][a-z0-9_]{0,57}$")
var RegexpPropName = regexp.MustCompile("^[a-z_][a-z0-9_./]{0,62}$")
var RegexpMapKey = regexp.MustCompile("^[a-z0-9][a-z0-9_.\\-]{0,62}$")
var RegexpID = regexp.MustCompile("^[a-zA-Z0-9_.\\-~]{1,62}$")
Expand All @@ -23,6 +24,10 @@ type ModelSerializer func(*Model, string) ([]byte, error)

var ModelSerializers = map[string]ModelSerializer{}

func IsValidModelName(name string) bool {
return RegexpModelName.MatchString(name)
}

func IsValidAttributeName(name string) bool {
return RegexpPropName.MatchString(name)
}
Expand Down Expand Up @@ -2187,6 +2192,11 @@ func ConvertString(val string, toType string) (any, bool) {
return nil, false
}

// 0=complete 1=GROUPS 2=RESOURCES|"" 3=versions|"" 4=[/versions]|""
// nil, or [0]="" means error
var targetREstr = `^(?:/([^/]+)(?:/([^[/]+)(?:(?:/(versions)|(\[(?:/versions)]))?))?)?$`
var targetRE = regexp.MustCompile(targetREstr)

func (attrs Attributes) Verify(ld *LevelData) error {
ld = &LevelData{
Model: ld.Model,
Expand Down Expand Up @@ -2235,38 +2245,23 @@ func (attrs Attributes) Verify(ld *LevelData) error {
"since \"type\" is \"relation\"", path.UI())
}
target := strings.TrimSpace(attr.Target)
if len(target) == 0 || target[0] != '/' {
parts := targetRE.FindStringSubmatch(target)
// 0=all 1=GROUPS 2=RESOURCES 3=versions|"" 4=[/versions]|""
if len(parts) == 0 || parts[0] == "" {
return fmt.Errorf("%q \"target\" must be of the form: "+
"/[GROUPS[/RESOURCES[/versions[?]]]]", path.UI())
"/GROUPS[/RESOURCES[/versions | \\[/versions\\] ]]",
path.UI())
}
target = target[1:]
if len(target) > 0 {
parts := strings.Split(target, "/")
if len(parts) > 3 || // too many /'s
len(parts[len(parts)-1]) == 0 { // ends with /
return fmt.Errorf("%q \"target\" must be of the form: "+
"/[GROUPS[/RESOURCES[/versions[?]]]]", path.UI())
}

gm := ld.Model.FindGroupModel(parts[0])
if gm == nil {
return fmt.Errorf("%q has an unknown Group type: %q",
path.UI(), parts[0])
}
if len(parts) > 1 {
rm := gm.Resources[parts[1]]
if rm == nil {
return fmt.Errorf("%q has an unknown Resource type: %q",
path.UI(), parts[1])
}

if len(parts) > 2 {
if parts[2] != "versions" && parts[2] != "versions?" {
return fmt.Errorf("%q \"target\" must be of "+
"the form: "+
"/[GROUPS[/RESOURCES[/versions[?]]]]", path.UI())
}
}
gm := ld.Model.FindGroupModel(parts[1])
if gm == nil {
return fmt.Errorf("%q has an unknown Group type: %q",
path.UI(), parts[1])
}
if parts[2] != "" {
if rm := gm.Resources[parts[2]]; rm == nil {
return fmt.Errorf("%q has an unknown Resource type: %q",
path.UI(), parts[2])
}
}
}
Expand Down
68 changes: 56 additions & 12 deletions registry/model_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package registry

import (
"fmt"
"reflect"
"testing"
)
Expand Down Expand Up @@ -104,11 +105,11 @@ func TestModelVerifyRegAttr(t *testing.T) {
{"err - type - relation - leading chars", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "xx/"}}},
`"model.x" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`},
`"model.x" "target" must be of the form: /GROUPS[/RESOURCES[/versions | \[/versions\] ]]`},
{"err - type - relation - extra / at end", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/xx/"}}},
`"model.x" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`},
`"model.x" "target" must be of the form: /GROUPS[/RESOURCES[/versions | \[/versions\] ]]`},
{"err - type - relation - spaces", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/ xx"}}},
Expand All @@ -125,10 +126,6 @@ func TestModelVerifyRegAttr(t *testing.T) {
Groups: groups},
`"model.x" has an unknown Resource type: "badr"`,
},
{"type - relation - reg", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/"}}, Groups: groups}, ``,
},
{"type - relation - group", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/dirs"}}, Groups: groups}, ``,
Expand All @@ -143,17 +140,18 @@ func TestModelVerifyRegAttr(t *testing.T) {
},
{"type - relation - both", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/dirs/files/versions?"}}, Groups: groups}, ``,
Target: "/dirs/files[/versions]"}}, Groups: groups}, ``,
},

{"type - relation - reg", Model{
{"type - relation - reg - ''", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: ""}}},
Target: ""}}, Groups: groups},
`"model.x" must have a "target" value since "type" is "relation"`},
{"type - relation - reg", Model{
{"type - relation - reg - /", Model{
Attributes: Attributes{"x": {Name: "x", Type: RELATION,
Target: "/"}}},
``},
Target: "/"}}, Groups: groups},
`"model.x" "target" must be of the form: /GROUPS[/RESOURCES[/versions | \[/versions\] ]]`,
},

{"type - string", Model{
Attributes: Attributes{"x": {Name: "x", Type: STRING}}}, ``},
Expand Down Expand Up @@ -499,3 +497,49 @@ func TestGetModelSerializer(t *testing.T) {
}
}
}

func TestTargetRegExp(t *testing.T) {
// targetRE
for _, test := range []struct {
input string
result []string
}{
{"", nil},
{"/", nil},
{"//versions", nil},
{"///versions", nil},
{"/g", []string{"g", "", "", ""}},
{"/g/", nil},
{"/g//versions", nil},
{"/g/[/versions]", nil},
{"/g/r", []string{"g", "r", "", ""}},
{"/g/r/", nil},
{"/g/r//", nil},
{"/g/r//versions", nil},
{"/g/r/[/versions]", nil},
{"/g/r/versions", []string{"g", "r", "versions", ""}},
{"/g/r//versions/", nil},
{"/g/r//versions[/versions]", nil},
{"/g/r[/versions]", []string{"g", "r", "", "[/versions]"}},
{"/g/r/[/versions]", nil},
{"/g/r[/versions]/", nil},
{"/g/r[/versions]/versions", nil},
} {
parts := targetRE.FindStringSubmatch(test.input)
tmpParts := []string{}
if len(parts) > 1 {
tmpParts = parts[1:]
}

exp := fmt.Sprintf("%#v", test.result)
got := fmt.Sprintf("%#v", tmpParts)

if (len(parts) == 0 || parts[0] == "") && test.result == nil {
continue
}

if len(tmpParts) != len(test.result) || exp != got {
t.Fatalf("\nIn: %s\nExp: %s\nGot: %s", test.input, exp, got)
}
}
}
Loading

0 comments on commit cc74adc

Please sign in to comment.