Refactor some reflection

This commit is contained in:
Jamie Curnow 2023-07-24 13:42:50 +10:00
parent d437c6b743
commit aae95798b2
No known key found for this signature in database
GPG Key ID: FFBB624C43388E9E
20 changed files with 287 additions and 300 deletions

View File

@ -9,7 +9,6 @@ import (
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/entity"
"npm/internal/model"
"npm/internal/tags"
"npm/internal/util"
@ -23,7 +22,7 @@ import (
// After we have determined what the Filters are to be, they are saved on the Context
// to be used later in other endpoints.
func ListQuery(obj interface{}) func(http.Handler) http.Handler {
schemaData := entity.GetFilterSchema(obj, true)
schemaData := tags.GetFilterSchema(obj)
filterMap := tags.GetFilterMap(obj)
return func(next http.Handler) http.Handler {

View File

@ -2,8 +2,8 @@ package accesslist
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/entity/user"
"npm/internal/model"
"npm/internal/types"
"github.com/rotisserie/eris"
@ -11,7 +11,7 @@ import (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Name string `json:"name" gorm:"column:name" filter:"name,string"`
Meta types.JSONB `json:"meta" gorm:"column:meta"`

View File

@ -2,7 +2,7 @@ package auth
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
"github.com/rotisserie/eris"
"golang.org/x/crypto/bcrypt"
@ -15,7 +15,7 @@ const (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id"`
Type string `json:"type" gorm:"column:type;default:password"`
Secret string `json:"secret,omitempty" gorm:"column:secret"`

View File

@ -10,11 +10,11 @@ import (
"npm/internal/acme"
"npm/internal/config"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/entity/user"
"npm/internal/logger"
"npm/internal/model"
"npm/internal/serverevents"
"npm/internal/types"
"npm/internal/util"
@ -44,7 +44,7 @@ const (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Type string `json:"type" gorm:"column:type" filter:"type,string"`
CertificateAuthorityID types.NullableDBUint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"`

View File

@ -5,15 +5,15 @@ import (
"path/filepath"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/errors"
"npm/internal/model"
"github.com/rotisserie/eris"
)
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
Name string `json:"name" gorm:"column:name" filter:"name,string"`
AcmeshServer string `json:"acmesh_server" gorm:"column:acmesh_server" filter:"acmesh_server,string"`
CABundle string `json:"ca_bundle" gorm:"column:ca_bundle" filter:"ca_bundle,string"`

View File

@ -5,8 +5,8 @@ import (
"npm/internal/database"
"npm/internal/dnsproviders"
"npm/internal/entity"
"npm/internal/logger"
"npm/internal/model"
"npm/internal/types"
"github.com/rotisserie/eris"
@ -14,7 +14,7 @@ import (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Name string `json:"name" gorm:"column:name" filter:"name,string"`
AcmeshName string `json:"acmesh_name" gorm:"column:acmesh_name" filter:"acmesh_name,string"`

View File

@ -9,7 +9,7 @@ import (
func GetFilterMap(m interface{}, includeBaseEntity bool) map[string]model.FilterMapValue {
filterMap := tags.GetFilterMap(m)
if includeBaseEntity {
return mergeFilterMaps(tags.GetFilterMap(ModelBase{}), filterMap)
return mergeFilterMaps(tags.GetFilterMap(model.ModelBase{}), filterMap)
}
return filterMap

View File

@ -1,252 +0,0 @@
package entity
import (
"fmt"
"reflect"
"strings"
"npm/internal/logger"
"npm/internal/util"
"github.com/rotisserie/eris"
)
// GetFilterSchema creates a jsonschema for validating filters, based on the model
// object given and by reading the struct "filter" tags.
func GetFilterSchema(m interface{}, includeBaseEntity bool) string {
var schemas []string
t := reflect.TypeOf(m)
if t.Kind() != reflect.Struct {
logger.Error("GetFilterSchemaError", eris.Errorf("%v type can't have attributes inspected", t.Kind()))
return ""
}
// The base entity model
if includeBaseEntity {
b := reflect.TypeOf(ModelBase{})
for i := 0; i < b.NumField(); i++ {
bField := b.Field(i)
bFilterTag := bField.Tag.Get("filter")
if bFilterTag != "" && bFilterTag != "-" {
schemas = append(schemas, getFilterTagSchema(bFilterTag))
}
}
}
// The actual interface
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
filterTag := field.Tag.Get("filter")
if filterTag != "" && filterTag != "-" {
schemas = append(schemas, getFilterTagSchema(filterTag))
}
}
return util.PrettyPrintJSON(newFilterSchema(schemas))
}
func getFilterTagSchema(filterTag string) string {
// split out tag value "field,filtreType"
// with a default filter type of string
items := strings.Split(filterTag, ",")
if len(items) == 1 {
items = append(items, "string")
}
switch items[1] {
case "number":
fallthrough
case "int":
fallthrough
case "integer":
return intFieldSchema(items[0])
case "bool":
fallthrough
case "boolean":
return boolFieldSchema(items[0])
case "date":
return dateFieldSchema(items[0])
case "regex":
if len(items) < 3 {
items = append(items, ".*")
}
return regexFieldSchema(items[0], items[2])
default:
return stringFieldSchema(items[0])
}
}
// newFilterSchema is the main method to specify a new Filter Schema for use in Middleware
func newFilterSchema(fieldSchemas []string) string {
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
}
// boolFieldSchema returns the Field Schema for a Boolean accepted value field
func boolFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, boolModifiers, filterBool, filterBool)
}
// intFieldSchema returns the Field Schema for a Integer accepted value field
func intFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^[0-9]+$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9]+$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// stringFieldSchema returns the Field Schema for a String accepted value field
func stringFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, stringModifiers, filterString, filterString)
}
// regexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
func regexFieldSchema(fieldName string, regex string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "%s"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "%s"
}
}
]
}
}
}`, fieldName, stringModifiers, regex, regex)
}
// dateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
func dateFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
const allModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
}`
const boolModifiers = `{
"type": "string",
"pattern": "^(equals|not)$"
}`
const stringModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
}`
const filterBool = `{
"type": "string",
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
}`
const filterString = `{
"type": "string",
"minLength": 1
}`
const baseFilterSchema = `{
"type": "array",
"items": {
"oneOf": [
%s
]
}
}`

View File

@ -3,11 +3,11 @@ package host
import (
"fmt"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/entity/certificate"
"npm/internal/entity/nginxtemplate"
"npm/internal/entity/upstream"
"npm/internal/entity/user"
"npm/internal/model"
"npm/internal/status"
"npm/internal/types"
"npm/internal/util"
@ -26,7 +26,7 @@ const (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Type string `json:"type" gorm:"column:type" filter:"type,string"`
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`

View File

@ -2,14 +2,14 @@ package nginxtemplate
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
"github.com/rotisserie/eris"
)
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Name string `json:"name" gorm:"column:name" filter:"name,string"`
Type string `json:"type" gorm:"column:type" filter:"type,string"`

View File

@ -4,14 +4,14 @@ import (
"strings"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
"gorm.io/datatypes"
)
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
Name string `json:"name" gorm:"column:name" filter:"name,string"`
Description string `json:"description" gorm:"column:description" filter:"description,string"`
Value datatypes.JSON `json:"value" gorm:"column:value"`

View File

@ -2,7 +2,7 @@ package stream
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
"npm/internal/types"
"github.com/rotisserie/eris"
@ -10,7 +10,7 @@ import (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
ExpiresOn types.DBDate `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"`
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Provider string `json:"provider" gorm:"column:provider" filter:"provider,string"`

View File

@ -4,10 +4,10 @@ import (
"strings"
"npm/internal/database"
"npm/internal/entity"
"npm/internal/entity/nginxtemplate"
"npm/internal/entity/upstreamserver"
"npm/internal/entity/user"
"npm/internal/model"
"npm/internal/status"
"npm/internal/util"
@ -17,7 +17,7 @@ import (
// Model is the model
// See: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
type Model struct {
entity.ModelBase
model.ModelBase
UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"`
Name string `json:"name" gorm:"column:name" filter:"name,string"`
NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"`

View File

@ -2,12 +2,12 @@ package upstreamserver
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
)
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
UpstreamID uint `json:"upstream_id" gorm:"column:upstream_id" filter:"upstream_id,integer"`
Server string `json:"server" gorm:"column:server" filter:"server,string"`
Weight int `json:"weight" gorm:"column:weight" filter:"weight,integer"`

View File

@ -7,6 +7,7 @@ import (
"npm/internal/entity"
"npm/internal/entity/auth"
"npm/internal/errors"
"npm/internal/model"
"npm/internal/util"
"github.com/drexedam/gravatar"
@ -15,7 +16,7 @@ import (
// Model is the model
type Model struct {
entity.ModelBase
model.ModelBase
Name string `json:"name" gorm:"column:name" filter:"name,string"`
Nickname string `json:"nickname" gorm:"column:nickname" filter:"nickname,string"`
Email string `json:"email" gorm:"column:email" filter:"email,email"`

View File

@ -2,14 +2,14 @@ package jwt
import (
"npm/internal/database"
"npm/internal/entity"
"npm/internal/model"
)
var currentKeys KeysModel
// KeysModel is the model
type KeysModel struct {
entity.ModelBase
model.ModelBase
PublicKey string `gorm:"column:public_key"`
PrivateKey string `gorm:"column:private_key"`
}

View File

@ -9,6 +9,8 @@ type Filter struct {
// FilterMapValue ...
type FilterMapValue struct {
Type string
Field string
Type string
Field string
Schema string
Model string
}

View File

@ -1,4 +1,4 @@
package entity
package model
import (
"gorm.io/plugin/soft_delete"

View File

@ -3,9 +3,9 @@ package nginx
import (
"testing"
"npm/internal/entity"
"npm/internal/entity/certificate"
"npm/internal/entity/host"
"npm/internal/model"
"npm/internal/types"
"github.com/stretchr/testify/assert"
@ -47,7 +47,7 @@ server {
IsDisabled: false,
},
cert: certificate.Model{
ModelBase: entity.ModelBase{
ModelBase: model.ModelBase{
ID: 77,
},
Status: certificate.StatusProvided,
@ -65,7 +65,7 @@ server {
IsDisabled: false,
},
cert: certificate.Model{
ModelBase: entity.ModelBase{
ModelBase: model.ModelBase{
ID: 66,
},
Status: certificate.StatusProvided,

View File

@ -1,11 +1,16 @@
package tags
import (
"fmt"
"reflect"
"regexp"
"strings"
"npm/internal/logger"
"npm/internal/model"
"npm/internal/util"
"github.com/rotisserie/eris"
)
func GetFilterMap(m interface{}) map[string]model.FilterMapValue {
@ -16,10 +21,21 @@ func GetFilterMap(m interface{}) map[string]model.FilterMapValue {
var filterMap = make(map[string]model.FilterMapValue)
// If this is an entity model (and it probably is)
// then include the base model as well
if strings.Contains(name, ".Model") && !strings.Contains(name, "ModelBase") {
filterMap = GetFilterMap(model.ModelBase{})
}
// TypeOf returns the reflection Type that represents the dynamic type of variable.
// If variable is a nil interface value, TypeOf returns nil.
t := reflect.TypeOf(m)
if t.Kind() != reflect.Struct {
logger.Error("GetFilterMapError", eris.Errorf("%v type can't have attributes inspected", t.Kind()))
return nil
}
// Iterate over all available fields and read the tag value
for i := 0; i < t.NumField(); i++ {
// Get the field, returns https://golang.org/pkg/reflect/#StructField
@ -28,26 +44,247 @@ func GetFilterMap(m interface{}) map[string]model.FilterMapValue {
// Get the field tag value
filterTag := field.Tag.Get("filter")
dbTag := field.Tag.Get("gorm")
if filterTag != "" && dbTag != "" && dbTag != "-" && filterTag != "-" {
// db can have many parts, we need to pull out the "column:value" part
dbField := field.Name
r := regexp.MustCompile(`(?:^|;)column:([^;|$]+)(?:$|;)`)
if matches := r.FindStringSubmatch(dbTag); len(matches) > 1 {
dbField = matches[1]
}
// Filter tag can be a 2 part thing: name,type
// ie: account_id,integer
// So we need to split and use the first part
f := model.FilterMapValue{
Model: name,
}
// Filter -> Schema mapping
if filterTag != "" && filterTag != "-" {
f.Schema = getFilterTagSchema(filterTag)
parts := strings.Split(filterTag, ",")
if len(parts) > 1 {
filterMap[parts[0]] = model.FilterMapValue{
Type: parts[1],
Field: dbField,
// Filter -> DB Field mapping
if dbTag != "" && dbTag != "-" {
// db can have many parts, we need to pull out the "column:value" part
f.Field = field.Name
r := regexp.MustCompile(`(?:^|;)column:([^;|$]+)(?:$|;)`)
if matches := r.FindStringSubmatch(dbTag); len(matches) > 1 {
f.Field = matches[1]
}
// Filter tag can be a 2 part thing: name,type
// ie: account_id,integer
// So we need to split and use the first part
if len(parts) > 1 {
f.Type = parts[1]
}
}
filterMap[parts[0]] = f
}
}
setCache(name, filterMap)
return filterMap
}
func getFilterTagSchema(filterTag string) string {
// split out tag value "field,filtreType"
// with a default filter type of string
items := strings.Split(filterTag, ",")
if len(items) == 1 {
items = append(items, "string")
}
switch items[1] {
case "number":
fallthrough
case "int":
fallthrough
case "integer":
return intFieldSchema(items[0])
case "bool":
fallthrough
case "boolean":
return boolFieldSchema(items[0])
case "date":
return dateFieldSchema(items[0])
case "regex":
if len(items) < 3 {
items = append(items, ".*")
}
return regexFieldSchema(items[0], items[2])
default:
return stringFieldSchema(items[0])
}
}
// GetFilterSchema creates a jsonschema for validating filters, based on the model
// object given and by reading the struct "filter" tags.
func GetFilterSchema(m interface{}) string {
filterMap := GetFilterMap(m)
schemas := make([]string, 0)
for _, f := range filterMap {
schemas = append(schemas, f.Schema)
}
str := fmt.Sprintf(baseFilterSchema, strings.Join(schemas, ", "))
return util.PrettyPrintJSON(str)
}
// boolFieldSchema returns the Field Schema for a Boolean accepted value field
func boolFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, boolModifiers, filterBool, filterBool)
}
// intFieldSchema returns the Field Schema for a Integer accepted value field
func intFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^[0-9]+$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9]+$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// stringFieldSchema returns the Field Schema for a String accepted value field
func stringFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, stringModifiers, filterString, filterString)
}
// regexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
func regexFieldSchema(fieldName string, regex string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "%s"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "%s"
}
}
]
}
}
}`, fieldName, stringModifiers, regex, regex)
}
// dateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
func dateFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
const allModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
}`
const boolModifiers = `{
"type": "string",
"pattern": "^(equals|not)$"
}`
const stringModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
}`
const filterBool = `{
"type": "string",
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
}`
const filterString = `{
"type": "string",
"minLength": 1
}`
const baseFilterSchema = `{
"type": "array",
"items": {
"oneOf": [
%s
]
}
}`