diff --git a/README.md b/README.md index 4dda2dfb..c0d71c50 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,4 @@ TODOs: - remove entities from cache upon delete - test creating a resource + lots of versions w/o ?defaultversionid-should fail - PUT ../f1 POST .../f1/versions POT .../versions +- Split the model.verify stuff so it doesn't verify the data unless asked to diff --git a/cmds/server/loader.go b/cmds/server/loader.go index a95f72ce..85ae23a5 100644 --- a/cmds/server/loader.go +++ b/cmds/server/loader.go @@ -246,8 +246,7 @@ func LoadDirsSample(reg *registry.Registry) *registry.Registry { ErrFatalf(err) _, err = reg.Model.AddAttrArray("arrmap", - registry.NewItemMap( - registry.NewItemType(registry.STRING))) + registry.NewItemMap(registry.NewItemType(registry.STRING))) ErrFatalf(err) ErrFatalf(reg.SetSave("bool1", true)) @@ -281,6 +280,9 @@ func LoadDirsSample(reg *registry.Registry) *registry.Registry { _, err = rm.AddAttr("*", registry.STRING) ErrFatalf(err) + _, err = reg.Model.AddAttrRelation("resptr", "/dirs/files/versions?") + ErrFatalf(err) + ErrFatalf(reg.Model.Verify()) g, err := reg.AddGroup("dirs", "dir1") @@ -295,6 +297,7 @@ func LoadDirsSample(reg *registry.Registry) *registry.Registry { ErrFatalf(r.SetSaveMeta("labels.none", "")) ErrFatalf(r.SetSaveMeta("rext", "a string")) ErrFatalf(r.SetSaveDefault("vext", "a ver string")) + ErrFatalf(reg.SetSave("resptr", "/dirs/dir1/files/f1/versions/v1")) _, err = g.AddResource("datas", "d1", "v1") @@ -328,60 +331,6 @@ func LoadEndpointsSample(reg *registry.Registry) *registry.Registry { err = reg.LoadModelFromFile(fn) ErrFatalf(err) - /* - ep, err := reg.Model.AddGroupModel("endpoints", "endpoint") - ErrFatalf(err) - attr, err := ep.AddAttr("usage", registry.STRING) - ErrFatalf(err) - // TODO make these required - // attr.ClientRequired = true - // attr.ServerRequired = true - _, err = ep.AddAttr("origin", registry.URI) - ErrFatalf(err) - _, err = ep.AddAttr("channel", registry.STRING) - ErrFatalf(err) - attr, err = ep.AddAttrObj("deprecated") - ErrFatalf(err) - _, err = attr.AddAttr("effective", registry.TIMESTAMP) - ErrFatalf(err) - _, err = attr.AddAttr("removal", registry.TIMESTAMP) - ErrFatalf(err) - _, err = attr.AddAttr("alternative", registry.URL) - ErrFatalf(err) - _, err = attr.AddAttr("docs", registry.URL) - ErrFatalf(err) - - config, err := attr.AddAttrObj("config") - ErrFatalf(err) - _, err = config.AddAttr("protocol", registry.STRING) - ErrFatalf(err) - obj, err := config.AddAttrMap("endpoints", registry.NewItemObject()) - ErrFatalf(err) - obj.Item.SetItem(registry.NewItem()) - _, err = obj.Item.Item.AddAttr("*", registry.ANY) - ErrFatalf(err) - - auth, err := config.AddAttrObj("authorization") - ErrFatalf(err) - attr, err = auth.AddAttr("type", registry.STRING) - ErrFatalf(err) - attr, err = auth.AddAttr("resourceurl", registry.STRING) - ErrFatalf(err) - attr, err = auth.AddAttr("authorityurl", registry.STRING) - ErrFatalf(err) - attr, err = auth.AddAttrArray("grant_types", registry.NewItemType(registry.STRING)) - ErrFatalf(err) - - _, err = config.AddAttr("strict", registry.BOOLEAN) - ErrFatalf(err) - - _, err = config.AddAttrMap("options", registry.NewItemType(registry.ANY)) - ErrFatalf(err) - - _, err = ep.AddResourceModel("definitions", "definition", 2, true, true, true) - ErrFatalf(err) - */ - // End of model g, err := reg.AddGroupWithObject("endpoints", "e1", registry.Object{ @@ -451,41 +400,7 @@ func LoadMessagesSample(reg *registry.Registry) *registry.Registry { err = reg.LoadModelFromFile(fn) ErrFatalf(err) - /* - msgs, _ := reg.Model.AddGroupModel("messagegroups", "messagegroup") - msgs.AddAttr("binding", registry.STRING) - - msg, _ := msgs.AddResourceModel("messages", "message", 1, true, true, false) - - // Modify core attribute - attr, _ := msg.AddAttr("format", registry.STRING) - attr.ClientRequired = true - attr.ServerRequired = true - - msg.AddAttr("basedefinitionurl", registry.URL) - - meta, _ := msg.AddAttrObj("metadata") - meta.AddAttr("required", registry.BOOLEAN) - meta.AddAttr("description", registry.STRING) - meta.AddAttr("value", registry.ANY) - meta.AddAttr("type", registry.STRING) - meta.AddAttr("specurl", registry.URL) - - obj := registry.NewItemObject() - meta.AddAttrMap("attributes", obj) - obj.AddAttr("type", registry.STRING) - obj.AddAttr("value", registry.ANY) - obj.AddAttr("required", registry.BOOLEAN) - - meta.AddAttr("binding", registry.STRING) - meta.AddAttrMap("message", registry.NewItemType(registry.ANY)) - - meta.AddAttr("schemaformat", registry.STRING) - meta.AddAttr("schema", registry.ANY) - meta.AddAttr("schemaurl", registry.URL) - - // End of model - */ + // End of model ErrFatalf(reg.Model.Verify()) reg.Commit() diff --git a/registry/const.go b/registry/const.go index c7e7338f..e1dc9d56 100644 --- a/registry/const.go +++ b/registry/const.go @@ -26,6 +26,7 @@ const DECIMAL = "decimal" const INTEGER = "integer" const MAP = "map" const OBJECT = "object" +const RELATION = "relation" const STRING = "string" const TIMESTAMP = "timestamp" const UINTEGER = "uinteger" diff --git a/registry/entity.go b/registry/entity.go index 71a2c4ab..32d0657d 100644 --- a/registry/entity.go +++ b/registry/entity.go @@ -856,6 +856,10 @@ var OrderedSpecProps = []*Attribute{ singular) } + if !IsValidID(newID.(string)) { + return fmt.Errorf(`%q isn't a valid ID`, newID) + } + if oldID != "" && !IsNil(oldID) && newID != oldID { return fmt.Errorf(`The %q attribute must be set to `+ `%q, not %q`, singular, oldID, newID) @@ -921,6 +925,10 @@ var OrderedSpecProps = []*Attribute{ return fmt.Errorf(`"versionid" can't be an empty string`) } + if !IsValidID(newID.(string)) { + return fmt.Errorf(`%q isn't a valid ID`, newID) + } + if oldID != "" && !IsNil(oldID) && newID != oldID { return fmt.Errorf(`The "versionid" attribute must be `+ `set to %q, not %q`, oldID, newID) @@ -2338,6 +2346,16 @@ func (e *Entity) ValidateScalar(val any, attr *Attribute, path *PropPath) error if i < 0 { return fmt.Errorf("Attribute %q must be a uinteger", path.UI()) } + case RELATION: + if valKind != reflect.String { + return fmt.Errorf("Attribute %q must be a relation", path.UI()) + } + str := val.(string) + + err := e.MatchRelation(str, attr.Target) + if err != nil { + return fmt.Errorf("Attribute %q %s", path.UI(), err.Error()) + } case STRING: if valKind != reflect.String { return fmt.Errorf("Attribute %q must be a string", path.UI()) @@ -2369,6 +2387,8 @@ func (e *Entity) ValidateScalar(val any, attr *Attribute, path *PropPath) error return fmt.Errorf("Attribute %q is a malformed timestamp", path.UI()) } + default: + panic(fmt.Sprintf("Unknown type: %v", attr.Type)) } // don't "return nil" above, we may need to check enum values @@ -2430,3 +2450,111 @@ func PrepUpdateEntity(e *Entity) error { return nil } + +// If no match then return an error saying why +func (e *Entity) MatchRelation(str string, relation string) error { + targetParts := strings.Split(relation, "/") + if len(str) == 0 { + return fmt.Errorf("must be an xid, not empty") + } + if str[0] != '/' { + return fmt.Errorf("must be an xid, and start with /") + } + strParts := strings.Split(str, "/") + if len(strParts) < 2 { + return fmt.Errorf("must be a valid xid") + } + if len(strParts[0]) > 0 { + return fmt.Errorf("must be an xid, and start with /") + } + if relation == "/" { + if str != "/" { + return fmt.Errorf("must match %q target", relation) + } + return nil + } + if targetParts[1] != strParts[1] { // works for "" too + return fmt.Errorf("must match %q target", relation) + } + + gm := e.Registry.Model.Groups[targetParts[1]] + if gm == nil { + return fmt.Errorf("uses an unknown group %q", targetParts[1]) + } + if len(strParts) < 3 || len(strParts[2]) == 0 { + return fmt.Errorf("must match %q target, missing \"%sid\"", + relation, gm.Singular) + } + if !IsValidID(strParts[2]) { + return fmt.Errorf("must match %q target, %q isn't a valid ID", + relation, strParts[2]) + } + + if len(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 + if len(strParts) < 4 { // /GROUPS/gID/RESOURCES + return fmt.Errorf("must match %q target, missing %q", + relation, targetParts[2]) + } + + if targetParts[2] != strParts[3] { + return fmt.Errorf("must match %q target, missing %q", + relation, targetParts[2]) + } + + rm := gm.Resources[targetParts[2]] + if rm == nil { + return fmt.Errorf("uses an unknown resource %q", targetParts[2]) + } + + if len(strParts) < 5 || len(strParts[4]) == 0 { + return fmt.Errorf("must match %q target, missing \"%sid\"", + relation, rm.Singular) + } + if !IsValidID(strParts[4]) { + return fmt.Errorf("must match %q target, %q isn't a valid ID", + relation, strParts[4]) + } + if len(targetParts) == 3 { + if len(strParts) == 5 { + return nil + } + return fmt.Errorf("must match %q target, extra stuff after %q", + relation, strParts[4]) + + } + + if targetParts[3] == "versions?" { + if len(strParts) == 5 { + // /GROUPS/RESOURCES/version? vs /GROUPS/gID/RESOURCES/rID + return nil + } + } + + if len(strParts) < 6 || strParts[5] != "versions" { + return fmt.Errorf("must match %q target, missing \"versions\"", + relation) + } + + if len(strParts) < 7 || len(strParts[6]) == 0 { + return fmt.Errorf("must match %q target, missing a \"versionid\"", + relation) + } + if !IsValidID(strParts[6]) { + return fmt.Errorf("must match %q target, %q isn't a valid ID", + relation, strParts[6]) + } + + if len(strParts) > 7 { + return fmt.Errorf("must match %q target, too long", relation) + } + + return nil +} diff --git a/registry/model.go b/registry/model.go index 1fd4002e..d69f6c5d 100644 --- a/registry/model.go +++ b/registry/model.go @@ -17,6 +17,7 @@ import ( 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}$") type ModelSerializer func(*Model, string) ([]byte, error) @@ -30,6 +31,10 @@ func IsValidMapKey(key string) bool { return RegexpMapKey.MatchString(key) } +func IsValidID(id string) bool { + return RegexpID.MatchString(id) +} + type Model struct { Registry *Registry `json:"-"` Labels map[string]string `json:"labels,omitempty"` @@ -57,17 +62,18 @@ type AttrInternals struct { // 'false', but Strict needs to default to 'true'. See the custome Unmarshal // funcs in model.go for how we set those type Attribute struct { - Registry *Registry `json:"-"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` - Enum []any `json:"enum,omitempty"` // just scalars though - Strict *bool `json:"strict,omitempty"` - ReadOnly bool `json:"readonly,omitempty"` - Immutable bool `json:"immutable,omitempty"` - ClientRequired bool `json:"clientrequired,omitempty"` - ServerRequired bool `json:"serverrequired,omitempty"` - Default any `json:"default,omitempty"` + Model *Model `json:"-"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Target string `json:"target,omitempty"` + Description string `json:"description,omitempty"` + Enum []any `json:"enum,omitempty"` // just scalars though + Strict *bool `json:"strict,omitempty"` + ReadOnly bool `json:"readonly,omitempty"` + Immutable bool `json:"immutable,omitempty"` + ClientRequired bool `json:"clientrequired,omitempty"` + ServerRequired bool `json:"serverrequired,omitempty"` + Default any `json:"default,omitempty"` Attributes Attributes `json:"attributes,omitempty"` // for Objs Item *Item `json:"item,omitempty"` // for maps & arrays @@ -81,7 +87,7 @@ type Attribute struct { } type Item struct { // for maps and arrays - Registry *Registry `json:"-"` + Model *Model `json:"-"` Type string `json:"type,omitempty"` Attributes Attributes `json:"attributes,omitempty"` // when 'type'=obj Item *Item `json:"item,omitempty"` // when 'type'=map,array @@ -94,8 +100,8 @@ type IfValue struct { } type GroupModel struct { - SID string `json:"-"` - Registry *Registry `json:"-"` + SID string `json:"-"` + Model *Model `json:"-"` Plural string `json:"plural"` Singular string `json:"singular"` @@ -230,18 +236,12 @@ func (r *ResourceModel) UnmarshalJSON(data []byte) error { } func (m *Model) SetPointers() { - PanicIf(m.Registry == nil, "Model.Registry can't be nil") - - if m.Attributes == nil { - m.Attributes = map[string]*Attribute{} - } - for _, attr := range m.Attributes { - attr.SetRegistry(m.Registry) + attr.SetModel(m) } for _, gm := range m.Groups { - gm.SetRegistry(m.Registry) + gm.SetModel(m) } } @@ -366,6 +366,10 @@ func (m *Model) AddAttrArray(name string, item *Item) (*Attribute, error) { return m.AddAttribute(&Attribute{Name: name, Type: ARRAY, Item: item}) } +func (m *Model) AddAttrRelation(name string, tgt string) (*Attribute, error) { + return m.AddAttribute(&Attribute{Name: name, Type: RELATION, Target: tgt}) +} + func (m *Model) AddAttribute(attr *Attribute) (*Attribute, error) { if attr == nil { return nil, nil @@ -379,14 +383,12 @@ func (m *Model) AddAttribute(attr *Attribute) (*Attribute, error) { m.Attributes = Attributes{} } - if attr.Registry == nil { - attr.Registry = m.Registry - } + attr.Model = m oldVal := m.Attributes[attr.Name] m.Attributes[attr.Name] = attr - attr.Item.SetRegistry(m.Registry) + attr.Item.SetModel(m) if err := m.VerifyAndSave(); err != nil { // Undo @@ -452,7 +454,7 @@ func (m *Model) AddGroupModel(plural string, singular string) (*GroupModel, erro } gm := &GroupModel{ SID: mSID, - Registry: m.Registry, + Model: m, Singular: singular, Plural: plural, @@ -511,50 +513,56 @@ func (m *Model) RemoveLabel(name string) error { } func NewItem() *Item { - return &Item{} + return &Item{ + // Model: m, // will be set when Item is added to attribute + } } func NewItemType(daType string) *Item { return &Item{ + // Model: m, // will be set when Item is added to attribute Type: daType, } } func NewItemObject() *Item { return &Item{ + // Model: m, // will be set when Item is added to attribute Type: OBJECT, } } func NewItemMap(item *Item) *Item { return &Item{ - Type: MAP, - Item: item, + Model: item.Model, + Type: MAP, + Item: item, } } func NewItemArray(item *Item) *Item { return &Item{ - Type: ARRAY, - Item: item, + Model: item.Model, + Type: ARRAY, + Item: item, } } -func (i *Item) SetRegistry(reg *Registry) { +func (i *Item) SetModel(m *Model) { if i == nil { return } - i.Registry = reg - i.Attributes.SetRegistry(reg) + i.Model = m + i.Attributes.SetModel(m) } func (i *Item) SetItem(item *Item) error { oldVal := i.Item i.Item = item - item.SetRegistry(i.Registry) + item.SetModel(i.Model) - if i.Registry != nil { - if err := i.Registry.Model.VerifyAndSave(); err != nil { + if i.Model != nil { + if err := i.Model.VerifyAndSave(); err != nil { // Undo i.Item = oldVal return err @@ -595,13 +603,11 @@ func (i *Item) AddAttribute(attr *Attribute) (*Attribute, error) { oldVal := i.Attributes[attr.Name] i.Attributes[attr.Name] = attr - if attr.Registry == nil { - attr.Registry = i.Registry - } - attr.Item.SetRegistry(i.Registry) + attr.Model = i.Model + attr.Item.SetModel(i.Model) - if i.Registry != nil { - if err := i.Registry.Model.VerifyAndSave(); err != nil { + if i.Model != nil { + if err := i.Model.VerifyAndSave(); err != nil { // Undo ResetMap(i.Attributes, attr.Name, oldVal) return nil, err @@ -619,8 +625,8 @@ func (i *Item) DelAttribute(name string) error { oldVal := i.Attributes[name] delete(i.Attributes, name) - if i.Registry != nil { - if err := i.Registry.Model.VerifyAndSave(); err != nil { + if i.Model != nil { + if err := i.Model.VerifyAndSave(); err != nil { // Undo ResetMap(i.Attributes, name, oldVal) return err @@ -664,7 +670,7 @@ func LoadModel(reg *Registry) *Model { } results.Close() - model.Attributes.SetRegistry(reg) + model.Attributes.SetModel(model) model.Attributes.SetSpecPropsFields("registry") // Load Groups & Resources @@ -704,7 +710,7 @@ func LoadModel(reg *Registry) *Model { if *row[2] == nil { // ParentSID nil -> new Group g := &GroupModel{ // Plural SID: NotNilString(row[0]), // SID - Registry: reg, + Model: model, Plural: NotNilString(row[3]), // Plural Singular: NotNilString(row[4]), // Singular Attributes: attrs, @@ -753,16 +759,31 @@ func LoadModel(reg *Registry) *Model { } func (m *Model) FindGroupModel(gTypePlural string) *GroupModel { + return m.Groups[gTypePlural] + /* + for _, gModel := range m.Groups { + if strings.EqualFold(gModel.Plural, gTypePlural) { + return gModel + } + } + return nil + */ +} + +/* +func (m *Model) FindGroupModelBySingular(gTypeSingular string) *GroupModel { for _, gModel := range m.Groups { - if strings.EqualFold(gModel.Plural, gTypePlural) { + if gModel.Singular == gTypeSingular { return gModel } } return nil } +*/ func (m *Model) ApplyNewModel(newM *Model) error { newM.Registry = m.Registry + if err := newM.Verify(); err != nil { return err } @@ -792,7 +813,7 @@ func (m *Model) ApplyNewModel(newM *Model) error { newM.Registry = m.Registry for _, newGM := range newM.Groups { log.VPrintf(4, "Applying Group: %s", newGM.Plural) - newGM.Registry = m.Registry + newGM.Model = m oldGM := m.Groups[newGM.Plural] if oldGM == nil { oldGM, err = m.AddGroupModel(newGM.Plural, newGM.Singular) @@ -850,16 +871,16 @@ func (m *Model) ApplyNewModel(newM *Model) error { func (gm *GroupModel) Delete() error { log.VPrintf(3, ">Enter: Delete.GroupModel: %s", gm.Plural) defer log.VPrintf(3, "Enter: Delete.ResourceModel: %s", rm.Plural) defer log.VPrintf(3, " 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()) + } + } + } + } + } + + if attr.Target != "" && attr.Type != RELATION { + return fmt.Errorf("%q must not have a \"target\" value "+ + "since \"type\" is not \"relation\"", path.UI()) + } + // Is it ok for strict=true and enum=[] ? Require no value??? // if attr.Strict == true && len(attr.Enum) == 0 { // } @@ -2266,7 +2346,7 @@ func (attrs Attributes) Verify(ld *LevelData) error { if attr.Item != nil { return fmt.Errorf("%q must not have an \"item\" section", path.UI()) } - if err := attr.Attributes.Verify(&LevelData{nil, path}); err != nil { + if err := attr.Attributes.Verify(&LevelData{ld.Model, nil, path}); err != nil { return err } } @@ -2289,7 +2369,9 @@ func (attrs Attributes) Verify(ld *LevelData) error { return fmt.Errorf("%q has an empty ifvalues key", ld.Path.UI()) } - nextLD := &LevelData{ld.AttrNames, + nextLD := &LevelData{ + ld.Model, + ld.AttrNames, ld.Path.P(attr.Name).P("ifvalues").P(valStr)} // Recursive @@ -2316,13 +2398,13 @@ func (attrs Attributes) SetSpecPropsFields(singular string) { } } -func (ifvalues IfValues) SetRegistry(reg *Registry) { +func (ifvalues IfValues) SetModel(m *Model) { if ifvalues == nil { return } for _, ifvalue := range ifvalues { - ifvalue.SiblingAttributes.SetRegistry(reg) + ifvalue.SiblingAttributes.SetModel(m) } } @@ -2349,7 +2431,7 @@ func (item *Item) Verify(path *PropPath) error { } if item.Attributes != nil { - if err := item.Attributes.Verify(&LevelData{nil, p}); err != nil { + if err := item.Attributes.Verify(&LevelData{item.Model, nil, p}); err != nil { return err } } @@ -2367,6 +2449,7 @@ var DefinedTypes = map[string]bool{ ARRAY: true, MAP: true, OBJECT: true, + RELATION: true, STRING: true, TIMESTAMP: true, URI: true, URI_REFERENCE: true, URI_TEMPLATE: true, URL: true} diff --git a/registry/model_test.go b/registry/model_test.go index c375ec63..d0e45df5 100644 --- a/registry/model_test.go +++ b/registry/model_test.go @@ -61,6 +61,19 @@ func TestModelVerifyRegAttr(t *testing.T) { err string } + groups := map[string]*GroupModel{ + "dirs": &GroupModel{ + Plural: "dirs", + Singular: "dir", + Resources: map[string]*ResourceModel{ + "files": &ResourceModel{ + Plural: "files", + Singular: "file", + }, + }, + }, + } + tests := []Test{ {"empty attrs", Model{Attributes: Attributes{}}, ""}, {"err - missing name", Model{ @@ -80,6 +93,68 @@ func TestModelVerifyRegAttr(t *testing.T) { Attributes: Attributes{"x": {Name: "x", Type: DECIMAL}}}, ``}, {"type - integer", Model{ Attributes: Attributes{"x": {Name: "x", Type: INTEGER}}}, ``}, + + {"err - type - relation - missing target", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION}}}, + `"model.x" must have a "target" value since "type" is "relation"`}, + + {"err - type - relation - extra target", Model{ + Attributes: Attributes{"x": {Name: "x", Type: STRING, Target: "/"}}}, + `"model.x" must not have a "target" value since "type" is not "relation"`}, + {"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[?]]]]`}, + {"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[?]]]]`}, + {"err - type - relation - spaces", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/ xx"}}}, + `"model.x" has an unknown Group type: " xx"`}, + {"err - type - relation - bad group", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/badg"}}, + Groups: groups}, + `"model.x" has an unknown Group type: "badg"`, + }, + {"err - type - relation - bad resource", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/dirs/badr"}}, + 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}, ``, + }, + {"type - relation - res", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/dirs/files"}}, Groups: groups}, ``, + }, + {"type - relation - versions", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/dirs/files/versions"}}, Groups: groups}, ``, + }, + {"type - relation - both", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/dirs/files/versions?"}}, Groups: groups}, ``, + }, + + {"type - relation - reg", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: ""}}}, + `"model.x" must have a "target" value since "type" is "relation"`}, + {"type - relation - reg", Model{ + Attributes: Attributes{"x": {Name: "x", Type: RELATION, + Target: "/"}}}, + ``}, + {"type - string", Model{ Attributes: Attributes{"x": {Name: "x", Type: STRING}}}, ``}, {"type - timestamp", Model{ diff --git a/registry/registry.go b/registry/registry.go index d3aaca3f..187bca8d 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -391,15 +391,13 @@ func (reg *Registry) LoadModelFromFile(file string) error { return fmt.Errorf("Processing %q: %s", file, err) } - model := &Model{ - Registry: reg, - } + model := &Model{} if err := Unmarshal(buf, model); err != nil { return fmt.Errorf("Processing %q: %s", file, err) } - model.SetPointers() + // TODO: Do we need to call model.SetPointers? if err := model.Verify(); err != nil { return fmt.Errorf("Processing %q: %s", file, err) diff --git a/tests/http_test.go b/tests/http_test.go index 0f7ffc1e..6c1041c7 100644 --- a/tests/http_test.go +++ b/tests/http_test.go @@ -14687,3 +14687,43 @@ func TestHTTPDefVer(t *testing.T) { }) } + +func TestHTTPInvalidID(t *testing.T) { + reg := NewRegistry("TestHTTPInvalidID") + defer PassDeleteReg(t, reg) + + gm, _ := reg.Model.AddGroupModel("dirs", "dir") + gm.AddResourceModelSimple("files", "file") + + xHTTP(t, reg, "PUT", "/", `{"registryid": "*" }`, 400, + "\"*\" isn't a valid ID\n") + + xHTTP(t, reg, "PUT", "/dirs/d1*", `{}`, 400, + "\"d1*\" isn't a valid ID\n") + xHTTP(t, reg, "PUT", "/dirs/d1", `{"dirid": "d1*" }`, 400, + "\"d1*\" isn't a valid ID\n") + xHTTP(t, reg, "POST", "/dirs/", `{"d1*":{}}`, 400, + "\"d1*\" isn't a valid ID\n") + xHTTP(t, reg, "POST", "/dirs/", `{"d1*":{"dirid": "d1*" }}`, 400, + "\"d1*\" isn't a valid ID\n") + xHTTP(t, reg, "POST", "/dirs/", `{"d1":{"dirid": "d2*" }}`, 400, + "\"d2*\" isn't a valid ID\n") + + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1*$structure", `{}`, 400, + "\"f1*\" isn't a valid ID\n") + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1$structure", `{"fileid":"f1*"}`, 400, + "The \"fileid\" attribute must be set to \"f1\", not \"f1*\"\n") + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1$structure", `{"versionid":"v1*"}`, + 400, "\"v1*\" isn't a valid ID\n") + + xHTTP(t, reg, "POST", "/dirs/d1/files/f1/versions", `{"v1*":{}}`, 400, + "\"v1*\" isn't a valid ID\n") + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1/versions/v1*", `{}`, 400, + "\"v1*\" isn't a valid ID\n") + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1/versions/v1$structure", + `{"versionid": "v1*"}`, 400, + "\"v1*\" isn't a valid ID\n") + xHTTP(t, reg, "PUT", "/dirs/d1/files/f1/versions/v1$structure", + `{"fileid": "f1*"}`, 400, + "\"f1*\" isn't a valid ID\n") +} diff --git a/tests/types_test.go b/tests/types_test.go index 644e3344..e33b5437 100644 --- a/tests/types_test.go +++ b/tests/types_test.go @@ -82,6 +82,50 @@ func TestBasicTypes(t *testing.T) { rm.AddAttr("filestring1", registry.STRING) rm.AddAttr("filestring2", registry.STRING) + _, err = reg.Model.AddAttrRelation("regptr_group", "") + xCheckErr(t, err, `"model.regptr_group" must have a "target" value since "type" is "relation"`) + _, err = reg.Model.AddAttrRelation("regptr_group", "qwe") + xCheckErr(t, err, `"model.regptr_group" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_group", "qwe/") + xCheckErr(t, err, `"model.regptr_group" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_group", " /") // ok? + xCheckErr(t, err, ``) + _, err = reg.Model.AddAttrRelation("regptr_group", " / ") // ok? + xCheckErr(t, err, ``) + reg.Model.AddAttrRelation("regptr_reg", "/") + + _, err = reg.Model.AddAttrRelation("regptr_group", "/xxxs") + xCheckErr(t, err, `"model.regptr_group" has an unknown Group type: "xxxs"`) + _, err = reg.Model.AddAttrRelation("regptr_group", "/xxxs/") + xCheckErr(t, err, `"model.regptr_group" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_group", "/dirs") + xCheckErr(t, err, ``) + + _, err = reg.Model.AddAttrRelation("regptr_res", "/dirs/?") + xCheckErr(t, err, `"model.regptr_res" has an unknown Resource type: "?"`) + _, err = reg.Model.AddAttrRelation("regptr_res", "/dirs/file") + xCheckErr(t, err, `"model.regptr_res" has an unknown Resource type: "file"`) + _, err = reg.Model.AddAttrRelation("regptr_res", "/dirs/files") + xCheckErr(t, err, ``) + + _, err = reg.Model.AddAttrRelation("regptr_ver", "/dirs/files/") + xCheckErr(t, err, `"model.regptr_ver" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_ver", "/dirs/files/asd") + xCheckErr(t, err, `"model.regptr_ver" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_ver", "/dirs/files/asd?") + xCheckErr(t, err, `"model.regptr_ver" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_ver", "/dirs/files/versions") + xCheckErr(t, err, ``) + + _, err = reg.Model.AddAttrRelation("regptr_res_ver", "/dirs/files/versions?asd") + xCheckErr(t, err, `"model.regptr_res_ver" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_res_ver", "/dirs/files/versions?/") + xCheckErr(t, err, `"model.regptr_res_ver" "target" must be of the form: /[GROUPS[/RESOURCES[/versions[?]]]]`) + _, err = reg.Model.AddAttrRelation("regptr_res_ver", "/dirs/files/versions?") + xCheckErr(t, err, ``) + _, err = reg.Model.AddAttrRelation("regptr_res_ver2", "/dirs/files/versions?") + xCheckErr(t, err, ``) + // Model is fully defined, so save it // reg.Model.Save() @@ -109,6 +153,9 @@ func TestBasicTypes(t *testing.T) { tests := []Test{ Test{reg, []Prop{ + {"registryid", 66, nil, `Attribute "registryid" must be a string`}, + {"registryid", "*", nil, `"*" isn't a valid ID`}, + {"regarrayarrayint[1][1]", 66, nil, "Attribute \"regarrayarrayint[1][0]\" must be an integer"}, {"regarrayint[0]", 1, nil, ""}, {"regarrayint[2]", 3, nil, "Attribute \"regarrayint[1]\" must be an integer"}, @@ -197,8 +244,74 @@ func TestBasicTypes(t *testing.T) { `Invalid extension(s): unknown_int`}, // unknown attr {"unknown_str", "error", nil, `Invalid extension(s): unknown_str`}, // unknown attr + + // regprt_reg _group _res _ver _res_ver + {"regptr_reg", 123, nil, + `Attribute "regptr_reg" must be a relation`}, + {"regptr_reg", "", nil, + `Attribute "regptr_reg" must be an xid, not empty`}, + {"regptr_reg", "asd", nil, + `Attribute "regptr_reg" must be an xid, and start with /`}, + {"regptr_reg", "asd/", nil, + `Attribute "regptr_reg" must be an xid, and start with /`}, + {"regptr_reg", "asd/dirs", nil, + `Attribute "regptr_reg" must be an xid, and start with /`}, + {"regptr_reg", "/dirs", nil, + `Attribute "regptr_reg" must match "/" target`}, + {"regptr_reg", "/", nil, ``}, + + {"regptr_group", "", nil, `Attribute "regptr_group" must be an xid, not empty`}, + {"regptr_group", "/", nil, `Attribute "regptr_group" must match "/dirs" target`}, + {"regptr_group", "/dirs2", nil, `Attribute "regptr_group" must match "/dirs" target`}, + {"regptr_group", "/dirs", nil, `Attribute "regptr_group" must match "/dirs" target, missing "dirid"`}, + {"regptr_group", "/dirs/*", nil, `Attribute "regptr_group" must match "/dirs" target, "*" isn't a valid ID`}, + {"regptr_group", "/dirs/id/", nil, `Attribute "regptr_group" must match "/dirs" target, extra stuff after "id"`}, + {"regptr_group", "/dirs/id/extra", nil, `Attribute "regptr_group" must match "/dirs" target, extra stuff after "id"`}, + {"regptr_group", "/dirs/id/extra/", nil, `Attribute "regptr_group" must match "/dirs" target, extra stuff after "id"`}, + {"regptr_group", "/dirs/d1", nil, ``}, + + {"regptr_res", "/dirs/d1", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "files"`}, + {"regptr_res", "/dirs/d1/", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "files"`}, + {"regptr_res", "/dirs/d1/fff", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "files"`}, + {"regptr_res", "/dirs/d1/fff/", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "files"`}, + {"regptr_res", "/dirs/d1/fff/f2", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "files"`}, + {"regptr_res", "/dirs/*/files/f2", nil, `Attribute "regptr_res" must match "/dirs/files" target, "*" isn't a valid ID`}, + {"regptr_res", "/dirs/d1/files", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "fileid"`}, + {"regptr_res", "/dirs/d1/files/", nil, `Attribute "regptr_res" must match "/dirs/files" target, missing "fileid"`}, + {"regptr_res", "/dirs/d1/files/*", nil, `Attribute "regptr_res" must match "/dirs/files" target, "*" isn't a valid ID`}, + {"regptr_res", "/dirs/d1/files/f2/versions", nil, `Attribute "regptr_res" must match "/dirs/files" target, extra stuff after "f2"`}, + {"regptr_res", "/dirs/d1/files/f2/versions/v1", nil, `Attribute "regptr_res" must match "/dirs/files" target, extra stuff after "f2"`}, + {"regptr_res", "/dirs/d1/files/f2", nil, ``}, + + {"regptr_ver", "/", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target`}, + {"regptr_ver", "/dirs/d1/files/f2", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, missing "versions"`}, + {"regptr_ver", "/dirs/d1/files/f2/vvv", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, missing "versions"`}, + {"regptr_ver", "/dirs/d1/files/f2/versions", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, missing a "versionid"`}, + {"regptr_ver", "/dirs/d1/files/f2/versions/", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, missing a "versionid"`}, + {"regptr_ver", "/dirs/d1/files/f2/versions/v2/", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, too long`}, + {"regptr_ver", "/dirs/d1/files/f2/versions/v2/xx", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, too long`}, + {"regptr_ver", "/dirs/d1/files/f2/versions/v2?", nil, `Attribute "regptr_ver" must match "/dirs/files/versions" target, "v2?" isn't a valid ID`}, + {"regptr_ver", "/dirs/d1/files/f2/versions/v2", nil, ``}, + + {"regptr_res_ver", "/dirs/d1/files/", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, missing "fileid"`}, + {"regptr_res_ver", "/dirs/d1/files//", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, missing "fileid"`}, + {"regptr_res_ver", "/dirs/d1/files/f2/", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, missing "versions"`}, + {"regptr_res_ver", "/dirs/d1/files/f2/vers", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, missing "versions"`}, + {"regptr_res_ver", "/dirs/d1/files/f2/vers/v1", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, missing "versions"`}, + {"regptr_res_ver", "/dirs/d1/files/f*/vers/v1", nil, `Attribute "regptr_res_ver" must match "/dirs/files/versions?" target, "f*" isn't a valid ID`}, + {"regptr_res_ver", "/dirs/d1/files/f2", nil, ``}, + + {"regptr_res_ver2", "/dirs/d1/files/f2/versions", nil, `Attribute "regptr_res_ver2" must match "/dirs/files/versions?" target, missing a "versionid"`}, + {"regptr_res_ver2", "/dirs/d1/files/f2/versions/", nil, `Attribute "regptr_res_ver2" must match "/dirs/files/versions?" target, missing a "versionid"`}, + {"regptr_res_ver2", "/dirs/d1/files/f2/versions//v2", nil, `Attribute "regptr_res_ver2" must match "/dirs/files/versions?" target, missing a "versionid"`}, + {"regptr_res_ver2", "/dirs/d1/files/f2/versions/v2/", nil, `Attribute "regptr_res_ver2" must match "/dirs/files/versions?" target, too long`}, + {"regptr_res_ver2", "/dirs/d1/files/f2/versions/v*", nil, `Attribute "regptr_res_ver2" must match "/dirs/files/versions?" target, "v*" isn't a valid ID`}, + {"regptr_res_ver2", "/dirs/d1/files/f2/versions/v2", nil, ``}, }}, Test{dir, []Prop{ + {"dirid", 66, nil, `Attribute "dirid" must be a string`}, + {"dirid", "*", nil, `"*" isn't a valid ID`}, + {"dirstring1", "str2", nil, ""}, {"dirstring2", "", nil, ""}, {"dirint1", 234, nil, ""}, @@ -220,6 +333,11 @@ func TestBasicTypes(t *testing.T) { {"dirobj", struct{}{}, map[string]any{}, ""}, }}, Test{file, []Prop{ + {"fileid", 66, nil, `Attribute "fileid" must be a string`}, + {"fileid", "*", nil, `"*" isn't a valid ID`}, + {"versionid", 66, nil, `Attribute "versionid" must be a string`}, + {"versionid", "*", nil, `"*" isn't a valid ID`}, + {"filestring1", "str3", nil, ""}, {"filestring2", "", nil, ""}, {"fileint1", 345, nil, ""}, @@ -233,6 +351,9 @@ func TestBasicTypes(t *testing.T) { {"filedec4", 0.0, nil, ""}, }}, Test{ver, []Prop{ + {"versionid", 66, nil, `Attribute "versionid" must be a string`}, + {"versionid", "*", nil, `"*" isn't a valid ID`}, + {"filestring1", "str4", nil, ""}, {"filestring2", "", nil, ""}, {"fileint1", 456, nil, ""}, @@ -261,12 +382,12 @@ func TestBasicTypes(t *testing.T) { // Note that for Resources this will set them on the default Version err := setter.SetSave(prop.Name, prop.Value) if err != nil && err.Error() != prop.ErrMsg { - t.Errorf("Error calling set (%q=%v): %q expected %q", prop.Name, - prop.Value, err, prop.ErrMsg) + t.Errorf("Error calling set (%q=%v):\nExp: %s\nGot: %s", + prop.Name, prop.Value, prop.ErrMsg, err) return // stop fast } if err == nil && prop.ErrMsg != "" { - t.Errorf("Setting (%q=%v) was supposed to fail: %s", + t.Errorf("Setting (%q=%v) was supposed to fail:\nGot: %s", prop.Name, prop.Value, prop.ErrMsg) return // stop fast } @@ -300,7 +421,7 @@ func TestBasicTypes(t *testing.T) { "registryid": "TestBasicTypes", "self": "http://localhost:8181/", "xid": "/", - "epoch": 3, + "epoch": 9, "createdat": "2024-01-01T12:00:01Z", "modifiedat": "2024-01-01T12:00:02Z", "reganyarrayint": [ @@ -357,6 +478,12 @@ func TestBasicTypes(t *testing.T) { }, "objstr": "in1" }, + "regptr_group": "/dirs/d1", + "regptr_reg": "/", + "regptr_res": "/dirs/d1/files/f2", + "regptr_res_ver": "/dirs/d1/files/f2", + "regptr_res_ver2": "/dirs/d1/files/f2/versions/v2", + "regptr_ver": "/dirs/d1/files/f2/versions/v2", "regstring1": "str1", "regstring2": "", "regtime1": "2006-01-02T15:04:05Z", @@ -369,9 +496,9 @@ func TestBasicTypes(t *testing.T) { "dirid": "d1", "self": "http://localhost:8181/dirs/d1", "xid": "/dirs/d1", - "epoch": 1, + "epoch": 2, "createdat": "2024-01-01T12:00:04Z", - "modifiedat": "2024-01-01T12:00:04Z", + "modifiedat": "2024-01-01T12:00:02Z", "dirbool1": true, "dirbool2": false, "dirdec1": 234.5, @@ -391,10 +518,10 @@ func TestBasicTypes(t *testing.T) { "versionid": "v1", "self": "http://localhost:8181/dirs/d1/files/f1$structure", "xid": "/dirs/d1/files/f1", - "epoch": 1, + "epoch": 3, "isdefault": true, "createdat": "2024-01-01T12:00:04Z", - "modifiedat": "2024-01-01T12:00:04Z", + "modifiedat": "2024-01-01T12:00:02Z", "filebool1": true, "filebool2": false, "filedec1": 456.5, @@ -426,10 +553,10 @@ func TestBasicTypes(t *testing.T) { "versionid": "v1", "self": "http://localhost:8181/dirs/d1/files/f1/versions/v1$structure", "xid": "/dirs/d1/files/f1/versions/v1", - "epoch": 1, + "epoch": 3, "isdefault": true, "createdat": "2024-01-01T12:00:04Z", - "modifiedat": "2024-01-01T12:00:04Z", + "modifiedat": "2024-01-01T12:00:02Z", "filebool1": true, "filebool2": false, "filedec1": 456.5,