Skip to content

Commit

Permalink
✨ feat: support field alias
Browse files Browse the repository at this point in the history
  • Loading branch information
0xE8551CCB committed Oct 8, 2019
1 parent ba576a5 commit ad054b5
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 35 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ func main() {
portal.Dump(&taskSchema, &taskModel, portal.Only("Title", "SimpleUser"))
// data: {"title":"Finish your jobs.","simple_user":{"name":"user:1"}}
data, _ := json.Marshal(taskSchema)

// select fields with alias defined in the json tag.
// actually, the default alias tag is `json`, `portal.FieldAliasMapTagName("json")` is optional.
portal.Dump(&taskSchema, &taskModel, portal.Only("title", "SimpleUser"), portal.FieldAliasMapTagName("json"))
// data: {"title":"Finish your jobs.","simple_user":{"name":"user:1"}}
data, _ := json.Marshal(taskSchema)

// you can keep any fields for any nested schemas
// multiple fields are separated with ','
Expand Down
17 changes: 11 additions & 6 deletions chell.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ import (

// Chell manages the dumping state.
type Chell struct {
onlyFieldFilters map[int][]*filterNode
excludeFieldFilters map[int][]*filterNode
// json, yaml etc.
fieldAliasMapTagName string
onlyFieldFilters map[int][]*filterNode
excludeFieldFilters map[int][]*filterNode
}

// New creates a new Chell instance with a worker pool waiting to be feed.
// It's highly recommended to call function `portal.Dump()` or
// `portal.DumpWithContext()` directly.
func New(opts ...Option) (*Chell, error) {
chell := &Chell{}
chell := &Chell{
fieldAliasMapTagName: "json",
}

for _, opt := range opts {
err := opt(chell)
if err != nil {
Expand Down Expand Up @@ -65,7 +70,7 @@ func (c *Chell) DumpWithContext(ctx context.Context, dst, src interface{}) error
extractFilterNodeNames(c.onlyFieldFilters[0], nil),
extractFilterNodeNames(c.excludeFieldFilters[0], &extractOption{ignoreNodeWithChildren: true}))
} else {
toSchema := newSchema(dst)
toSchema := newSchema(dst).withFieldAliasMapTagName(c.fieldAliasMapTagName)
toSchema.setOnlyFields(extractFilterNodeNames(c.onlyFieldFilters[0], nil)...)
toSchema.setExcludeFields(extractFilterNodeNames(c.excludeFieldFilters[0], &extractOption{ignoreNodeWithChildren: true})...)
return c.dump(incrDumpDepthContext(ctx), toSchema, src)
Expand Down Expand Up @@ -215,7 +220,7 @@ func (c *Chell) dumpField(ctx context.Context, field *schemaField, value interfa

func (c *Chell) dumpFieldNestedOne(ctx context.Context, field *schemaField, src interface{}) error {
val := reflect.New(indirectStructTypeP(reflect.TypeOf(field.Value())))
toNestedSchema := newSchema(val.Interface())
toNestedSchema := newSchema(val.Interface()).withFieldAliasMapTagName(c.fieldAliasMapTagName)

depth := dumpDepthFromContext(ctx)
toNestedSchema.setOnlyFields(field.nestedOnlyNames(c.onlyFieldFilters[depth])...)
Expand Down Expand Up @@ -293,7 +298,7 @@ func (c *Chell) dumpMany(ctx context.Context, dst, src interface{}, onlyFields,
func(payload interface{}) (interface{}, error) {
index := payload.(int)
schemaPtr := reflect.New(schemaType)
toSchema := newSchema(schemaPtr.Interface())
toSchema := newSchema(schemaPtr.Interface()).withFieldAliasMapTagName(c.fieldAliasMapTagName)
toSchema.setOnlyFields(onlyFields...)
toSchema.setExcludeFields(excludeFields...)
val := rv.Index(index).Interface()
Expand Down
8 changes: 4 additions & 4 deletions examples/todo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ func main() {
}

printFullFields(&task)
//printWithOnlyFields(&task, "Title", "SimpleUser")
//printWithOnlyFields(&task, "ID", "User[ID,Notifications[ID],AnotherNotifications[Title]]", "SimpleUser")
//printMany()
//printWithExcludeFields(&task, "Description", "ID", "User[Name,Notifications[ID,Content],AnotherNotifications], SimpleUser")
printWithOnlyFields(&task, "Title", "SimpleUser")
printWithOnlyFields(&task, "ID", "User[ID,Notifications[ID],AnotherNotifications[Title]]", "SimpleUser")
printMany()
printWithExcludeFields(&task, "Description", "ID", "User[Name,Notifications[ID,Content],AnotherNotifications], SimpleUser")
fmt.Printf("elapsed: %.1f ms\n", time.Since(start).Seconds()*1000)
}

Expand Down
30 changes: 30 additions & 0 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package portal

import (
"database/sql/driver"
"regexp"
"strings"
"sync"

Expand All @@ -13,13 +14,15 @@ import (
var (
defaultTagName = "portal"
cachedFieldTagSettings sync.Map
cachedAliasMap sync.Map
)

type schemaField struct {
*structs.Field
settings map[string]string
isIgnored bool //nolint
schema *schema
alias string
}

func newField(schema *schema, field *structs.Field) *schemaField {
Expand All @@ -40,6 +43,7 @@ func newField(schema *schema, field *structs.Field) *schemaField {
Field: field,
schema: schema,
settings: settings,
alias: parseAlias(field.Tag(schema.fieldAliasMapTagName)),
}
}

Expand Down Expand Up @@ -227,3 +231,29 @@ func parseTagSettings(s string) map[string]string {
}
return settings
}

func parseAlias(s string) string {
ret, ok := cachedAliasMap.Load(s)
if ok {
return ret.(string)
}

parts := strings.Split(s, ",")
if len(parts) == 0 {
cachedAliasMap.Store(s, "")
return ""
}
alias := strings.TrimSpace(parts[0])
re, err := regexp.Compile(`^[_a-zA-Z0-9]+-*[_a-zA-Z0-9]+$`)
if err != nil {
cachedAliasMap.Store(s, "")
return ""
}
if re.MatchString(alias) {
cachedAliasMap.Store(s, alias)
return alias
}

cachedAliasMap.Store(s, "")
return ""
}
27 changes: 27 additions & 0 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ func TestField_String(t *testing.T) {
assert.Equal(t, "FooSchema.Name", f.String())
}

func TestField_Alias(t *testing.T) {
type FooSchema struct {
Name string `json:"name"`
}

schema := newSchema(&FooSchema{}).withFieldAliasMapTagName("json")
f := newField(schema, schema.innerStruct().Field("Name"))
assert.Equal(t, "name", f.alias)
}

func TestField_IsRequired(t *testing.T) {
type FooSchema struct {
Name string
Expand Down Expand Up @@ -258,3 +268,20 @@ func BenchmarkNewField(b *testing.B) {
newField(schema, schema.innerStruct().Field("Name"))
}
}

func Test_parseAlias(t *testing.T) {
assert.Equal(t, "", parseAlias(""))
assert.Equal(t, "", parseAlias("-"))
assert.Equal(t, "id", parseAlias("id,empty"))
assert.Equal(t, "urlToken", parseAlias("urlToken,empty"))
assert.Equal(t, "url_token", parseAlias("url_token,empty"))
assert.Equal(t, "url-token", parseAlias(" url-token, empty "))
}

// Benchmark_parseAlias-4 35453392 34.5 ns/op
func Benchmark_parseAlias(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
parseAlias(" url-token, empty ")
}
}
16 changes: 16 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,19 @@ func Exclude(fields ...string) Option {
return nil
}
}

// FieldAliasMapTagName sets the tag name (e.g. `yaml`, `json`) to parse alias of a field name.
// Example:
// ```
// struct Schema {
// ID int `json:"id"`
// }
//
// // portal parses the json tag, and maps `id` -> `ID`.
// ```
func FieldAliasMapTagName(tag string) Option {
return func(c *Chell) error {
c.fieldAliasMapTagName = tag
return nil
}
}
68 changes: 44 additions & 24 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
)

type schema struct {
RawValue interface{}
schemaStruct *structs.Struct
availableFieldNames map[string]bool
fieldAliasMapTagName string
rawValue interface{}
schemaStruct *structs.Struct
availableFieldNames map[string]bool
fields []*schemaField
}

func newSchema(v interface{}) *schema {
Expand Down Expand Up @@ -40,18 +42,25 @@ func newSchema(v interface{}) *schema {
}

sch := &schema{
schemaStruct: structs.New(schemaValue.Addr().Interface()),
availableFieldNames: make(map[string]bool),
RawValue: schemaValue.Addr().Interface(),
schemaStruct: structs.New(schemaValue.Addr().Interface()),
availableFieldNames: make(map[string]bool),
rawValue: schemaValue.Addr().Interface(),
fieldAliasMapTagName: "json",
}

for _, name := range getAvailableFieldNames(sch.schemaStruct.Fields()) {
sch.availableFieldNames[name] = true
sch.fields = append(sch.fields, newField(sch, sch.schemaStruct.Field(name)))
}

return sch
}

func (s *schema) withFieldAliasMapTagName(t string) *schema {
s.fieldAliasMapTagName = t
return s
}

func getAvailableFieldNames(fields []*structs.Field) (names []string) {
for _, f := range fields {
if f.IsEmbedded() {
Expand All @@ -65,12 +74,12 @@ func getAvailableFieldNames(fields []*structs.Field) (names []string) {

func (s *schema) availableFields() []*schemaField {
fields := make([]*schemaField, 0)
for k, v := range s.availableFieldNames {
if v {
fields = append(fields, newField(s, s.schemaStruct.Field(k)))
for _, f := range s.fields {
v, ok := s.availableFieldNames[f.Name()]
if ok && v {
fields = append(fields, f)
}
}

return fields
}

Expand Down Expand Up @@ -105,7 +114,7 @@ func (s *schema) fieldValueFromSrc(ctx context.Context, field *schemaField, v in
if field.hasConstValue() {
val = field.constValue()
} else if field.hasMethod() {
ret, err := invokeStructMethod(ctx, s.RawValue, field.method(), v)
ret, err := invokeStructMethod(ctx, s.rawValue, field.method(), v)
if err != nil {
return nil, fmt.Errorf("failed to get value: %s", err)
}
Expand All @@ -130,38 +139,49 @@ func (s *schema) fieldValueFromSrc(ctx context.Context, field *schemaField, v in
return
}

func (s *schema) setOnlyFields(fields ...string) {
if len(fields) == 0 {
func (s *schema) setOnlyFields(fieldNames ...string) {
if len(fieldNames) == 0 {
return
}

for k := range s.availableFieldNames {
s.availableFieldNames[k] = false
}

for _, f := range fields {
if _, ok := s.availableFieldNames[f]; ok {
s.availableFieldNames[f] = true
for _, f := range fieldNames {
field := s.fieldByNameOrAlias(f)
if field == nil {
logger.Warnf("field name '%s.%s' not found", s.name(), f)
} else {
panic(fmt.Sprintf("field name '%s.%s' not found", s.name(), f))
s.availableFieldNames[field.Name()] = true
}
}
}

func (s *schema) setExcludeFields(fields ...string) {
if len(fields) == 0 {
func (s *schema) setExcludeFields(fieldNames ...string) {
if len(fieldNames) == 0 {
return
}

for _, f := range fields {
if _, ok := s.availableFieldNames[f]; ok {
s.availableFieldNames[f] = false
for _, f := range fieldNames {
field := s.fieldByNameOrAlias(f)
if field == nil {
logger.Warnf("field name '%s.%s' not found", s.name(), f)
} else {
panic(fmt.Sprintf("field name '%s' not found", f))
s.availableFieldNames[field.Name()] = false
}
}
}

func (s *schema) name() string {
return structName(s.RawValue)
return structName(s.rawValue)
}

func (s *schema) fieldByNameOrAlias(name string) *schemaField {
for _, f := range s.fields {
if f.alias == name || f.Name() == name {
return f
}
}
return nil
}
11 changes: 10 additions & 1 deletion schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestSchema(t *testing.T) {
}

func TestSchema_GetFields(t *testing.T) {
schema := newSchema(&UserSchema2{})
schema := newSchema(&UserSchema2{}).withFieldAliasMapTagName("json")
assert.ElementsMatch(t, []string{"Age", "ID", "Name", "School", "Async"}, filedNames(schema.availableFields()))

assert.ElementsMatch(t, []string{"Age", "ID", "Name", "School"}, filedNames(schema.syncFields()))
Expand All @@ -64,9 +64,18 @@ func TestSchema_GetFields(t *testing.T) {
schema.setOnlyFields("ID")
assert.ElementsMatch(t, []string{"ID"}, filedNames(schema.availableFields()))

schema.setOnlyFields("id")
assert.ElementsMatch(t, []string{"ID"}, filedNames(schema.availableFields()))

schema.setOnlyFields("ID", "NotFound")
assert.ElementsMatch(t, []string{"ID"}, filedNames(schema.availableFields()))

schema = newSchema(&UserSchema2{})
schema.setExcludeFields("ID", "Name", "School")
assert.ElementsMatch(t, []string{"Age", "Async"}, filedNames(schema.availableFields()))

schema.setExcludeFields("ID", "Name", "School", "NotFound")
assert.ElementsMatch(t, []string{"Age", "Async"}, filedNames(schema.availableFields()))
}

func filedNames(fields []*schemaField) (names []string) {
Expand Down

0 comments on commit ad054b5

Please sign in to comment.